From 554ed39499fdc6200e473456ce6cc58372e2c90b Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Mon, 16 Jun 2025 13:53:55 +0200 Subject: [PATCH 1/8] Adding base MCP Server --- .../McpToolAttribute.cs | 39 +++++++++ .../McpToolRegistry.cs | 46 +++++++++++ .../McpToolsJsonHelper.cs | 80 +++++++++++++++++++ .../Properties/AssemblyInfo.cs | 42 ++++++++++ nanoFramework.WebServer.Mcp/ToolMetadata.cs | 17 ++++ .../nanoFramework.WebServer.Mcp.nfproj | 56 +++++++++++++ nanoFramework.WebServer.Mcp/packages.config | 9 +++ .../packages.lock.json | 43 ++++++++++ nanoFramework.WebServer.sln | 26 ++++++ tests/McpEndToEndTest/McpEndToEndTest.nfproj | 66 +++++++++++++++ tests/McpEndToEndTest/McpToolsClasses.cs | 33 ++++++++ tests/McpEndToEndTest/Program.cs | 22 +++++ .../Properties/AssemblyInfo.cs | 36 +++++++++ tests/McpEndToEndTest/packages.config | 12 +++ tests/McpEndToEndTest/packages.lock.json | 61 ++++++++++++++ tests/McpServerTests/McpServerTests.nfproj | 54 +++++++++++++ .../McpServerTests/McpToolsAttributeTests.cs | 24 ++++++ tests/McpServerTests/McpToolsClasses.cs | 33 ++++++++ .../McpServerTests/Properties/AssemblyInfo.cs | 34 ++++++++ tests/McpServerTests/nano.runsettings | 17 ++++ tests/McpServerTests/packages.config | 5 ++ tests/McpServerTests/packages.lock.json | 19 +++++ 22 files changed, 774 insertions(+) create mode 100644 nanoFramework.WebServer.Mcp/McpToolAttribute.cs create mode 100644 nanoFramework.WebServer.Mcp/McpToolRegistry.cs create mode 100644 nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs create mode 100644 nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs create mode 100644 nanoFramework.WebServer.Mcp/ToolMetadata.cs create mode 100644 nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj create mode 100644 nanoFramework.WebServer.Mcp/packages.config create mode 100644 nanoFramework.WebServer.Mcp/packages.lock.json create mode 100644 tests/McpEndToEndTest/McpEndToEndTest.nfproj create mode 100644 tests/McpEndToEndTest/McpToolsClasses.cs create mode 100644 tests/McpEndToEndTest/Program.cs create mode 100644 tests/McpEndToEndTest/Properties/AssemblyInfo.cs create mode 100644 tests/McpEndToEndTest/packages.config create mode 100644 tests/McpEndToEndTest/packages.lock.json create mode 100644 tests/McpServerTests/McpServerTests.nfproj create mode 100644 tests/McpServerTests/McpToolsAttributeTests.cs create mode 100644 tests/McpServerTests/McpToolsClasses.cs create mode 100644 tests/McpServerTests/Properties/AssemblyInfo.cs create mode 100644 tests/McpServerTests/nano.runsettings create mode 100644 tests/McpServerTests/packages.config create mode 100644 tests/McpServerTests/packages.lock.json diff --git a/nanoFramework.WebServer.Mcp/McpToolAttribute.cs b/nanoFramework.WebServer.Mcp/McpToolAttribute.cs new file mode 100644 index 0000000..73b04a3 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpToolAttribute.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Reflection; +using System.Text; + +namespace nanoFramework.WebServer.Mcp +{ + [AttributeUsage(AttributeTargets.Method)] + public class McpToolAttribute : Attribute + { + public string Name { get; } + public string Description { get; } + public string InputType { get; internal set; } + public string OutputType { get; internal set; } + public Type DefaultValue { get; } + + public McpToolAttribute(string name, string description, string inputType = null, string outputType = null, Type defaultValue = null) + { + Name = name; + Description = description; + InputType = inputType; + OutputType = outputType; + DefaultValue = defaultValue; + } + + public void SetInputType(object inputType) + { + InputType = McpToolJsonHelper.GenerateInputJson(inputType.GetType()); + } + + public void SetOutputType(string outputType) + { + OutputType = McpToolJsonHelper.GenerateInputJson(outputType.GetType()); + } + } +} diff --git a/nanoFramework.WebServer.Mcp/McpToolRegistry.cs b/nanoFramework.WebServer.Mcp/McpToolRegistry.cs new file mode 100644 index 0000000..39c9b88 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpToolRegistry.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Reflection; +using nanoFramework.Json; +using System.Threading; + +namespace nanoFramework.WebServer.Mcp +{ + public static class McpToolRegistry + { + private static readonly Hashtable tools = new Hashtable(); + + public static void DiscoverTools(Type[] mcpTools) + { + foreach (Type mcpTool in mcpTools) + { + MethodInfo[] methods = mcpTool.GetMethods(BindingFlags.Public | BindingFlags.Static); + foreach (MethodInfo method in methods) + { + var allAttribute = method.GetCustomAttributes(false); + foreach (var attrib in allAttribute) + { + if (attrib.GetType() == typeof(McpToolAttribute)) + { + McpToolAttribute attribute = (McpToolAttribute)attrib; + if (attribute != null) + { + tools[attribute.Name] = new ToolMetadata + { + Name = attribute.Name, + Description = attribute.Description, + InputType = attribute.InputType, + OutputType = attribute.OutputType, + Method = method + }; + } + } + } + } + } + } + } +} diff --git a/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs b/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs new file mode 100644 index 0000000..d8caf1f --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Text; + +namespace nanoFramework.WebServer.Mcp +{ + public static class McpToolJsonHelper + { + public static string GenerateInputJson(Type inputType) + { + StringBuilder sb = new StringBuilder(); + sb.Append("["); + AppendPropertiesJson(sb, inputType, true); + sb.Append("]"); + return sb.ToString(); + } + + private static void AppendPropertiesJson(StringBuilder sb, Type type, bool isFirst) + { + MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); + // Find all property names by looking for get_ methods + for (int i = 0; i < methods.Length; i++) + { + MethodInfo method = methods[i]; + if (method.Name.StartsWith("get_") && method.GetParameters().Length == 0) + { + string propName = method.Name.Substring(4); + Type propType = method.ReturnType; + + if (!isFirst) sb.Append(","); + isFirst = false; + + sb.Append("{"); + sb.Append("\"name\":\"").Append(propName).Append("\","); + string mappedType = MapType(propType); + sb.Append("\"type\":\"").Append(mappedType).Append("\","); + sb.Append("\"description\":\"").Append(propName).Append("\""); + if (mappedType == "object") + { + sb.Append(",\"properties\":["); + AppendPropertiesJson(sb, propType, true); + sb.Append("]"); + } + sb.Append("}"); + } + } + } + + private static string MapType(Type type) + { + if (type == typeof(string)) + { + return "string"; + } + else if (type == typeof(int) || type == typeof(double) || type == typeof(float) || + type == typeof(long) || type == typeof(short) || type == typeof(byte)) + { + return "number"; + } + else if (type == typeof(bool)) + { + return "boolean"; + } + else if (type.IsArray) + { + return "array"; + } + else + if (type.IsClass && type != typeof(string)) + { + return "object"; + } + + return "string"; + } + } +} diff --git a/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs b/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..da9340e --- /dev/null +++ b/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CSharp.TestApplication")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CSharp.TestApplication")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +///////////////////////////////////////////////////////////////// +// This attribute is mandatory when building Interop libraries // +// update this whenever the native assembly signature changes // +[assembly: AssemblyNativeVersion("1.0.0.0")] +///////////////////////////////////////////////////////////////// diff --git a/nanoFramework.WebServer.Mcp/ToolMetadata.cs b/nanoFramework.WebServer.Mcp/ToolMetadata.cs new file mode 100644 index 0000000..46e11cb --- /dev/null +++ b/nanoFramework.WebServer.Mcp/ToolMetadata.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; + +namespace nanoFramework.WebServer.Mcp +{ + public class ToolMetadata + { + public string Name { get; set; } + public string Description { get; set; } + public string InputType { get; set; } + public string OutputType { get; set; } + public MethodInfo Method { get; set; } + } +} diff --git a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj new file mode 100644 index 0000000..3ae8897 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj @@ -0,0 +1,56 @@ + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 5997f9e4-ae44-47e9-a0f5-abe7fd8be59e + Library + Properties + 512 + nanoFramework.WebServer.Mcp + nanoFramework.WebServer.Mcp + v1.0 + + + + + + + + + + + + ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll + + + ..\packages\nanoFramework.System.Collections.1.5.67\lib\nanoFramework.System.Collections.dll + + + ..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nanoFramework.WebServer.Mcp/packages.config b/nanoFramework.WebServer.Mcp/packages.config new file mode 100644 index 0000000..ea1e821 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/nanoFramework.WebServer.Mcp/packages.lock.json b/nanoFramework.WebServer.Mcp/packages.lock.json new file mode 100644 index 0000000..df144f1 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/packages.lock.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.Json": { + "type": "Direct", + "requested": "[2.2.199, 2.2.199]", + "resolved": "2.2.199", + "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w==" + }, + "nanoFramework.System.Collections": { + "type": "Direct", + "requested": "[1.5.67, 1.5.67]", + "resolved": "1.5.67", + "contentHash": "MjSipUB70vrxjqTm1KfKTUqqjd0wbweiNyYFXONi0XClrH6HXsuX2lhDqXM8NWuYnWyYOqx8y20sXbvsH+4brg==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.7.115, 3.7.115]", + "resolved": "3.7.115", + "contentHash": "EpXamaAdRfG/BMxGgvZlTM0npRnkmXUjAj8OdNKd17t4oN+2nvjdv/KnFmzOOMDqvlwB49UCwtOHJrAQTfUBtQ==" + } + } + } +} \ No newline at end of file diff --git a/nanoFramework.WebServer.sln b/nanoFramework.WebServer.sln index b6e6c3b..445ed0a 100644 --- a/nanoFramework.WebServer.sln +++ b/nanoFramework.WebServer.sln @@ -19,6 +19,12 @@ Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "WebServerE2ETests", "tests\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E76226D2-994C-4EE1-B346-050F31B175BD}" EndProject +Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "nanoFramework.WebServer.Mcp", "nanoFramework.WebServer.Mcp\nanoFramework.WebServer.Mcp.nfproj", "{5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}" +EndProject +Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "McpServerTests", "tests\McpServerTests\McpServerTests.nfproj", "{4BC7B119-BF3F-4FED-84BD-5909543343D5}" +EndProject +Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "McpEndToEndTest", "tests\McpEndToEndTest\McpEndToEndTest.nfproj", "{1CDBEB80-6DDF-494B-9A26-47E889E523A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,6 +55,24 @@ Global {A0611EAD-FB04-44E7-BAD3-459DD0A7FF46}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0611EAD-FB04-44E7-BAD3-459DD0A7FF46}.Release|Any CPU.Build.0 = Release|Any CPU {A0611EAD-FB04-44E7-BAD3-459DD0A7FF46}.Release|Any CPU.Deploy.0 = Release|Any CPU + {5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}.Release|Any CPU.Build.0 = Release|Any CPU + {5997F9E4-AE44-47E9-A0F5-ABE7FD8BE59E}.Release|Any CPU.Deploy.0 = Release|Any CPU + {4BC7B119-BF3F-4FED-84BD-5909543343D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BC7B119-BF3F-4FED-84BD-5909543343D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4BC7B119-BF3F-4FED-84BD-5909543343D5}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {4BC7B119-BF3F-4FED-84BD-5909543343D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BC7B119-BF3F-4FED-84BD-5909543343D5}.Release|Any CPU.Build.0 = Release|Any CPU + {4BC7B119-BF3F-4FED-84BD-5909543343D5}.Release|Any CPU.Deploy.0 = Release|Any CPU + {1CDBEB80-6DDF-494B-9A26-47E889E523A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CDBEB80-6DDF-494B-9A26-47E889E523A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CDBEB80-6DDF-494B-9A26-47E889E523A9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {1CDBEB80-6DDF-494B-9A26-47E889E523A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CDBEB80-6DDF-494B-9A26-47E889E523A9}.Release|Any CPU.Build.0 = Release|Any CPU + {1CDBEB80-6DDF-494B-9A26-47E889E523A9}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -56,6 +80,8 @@ Global GlobalSection(NestedProjects) = preSolution {2C2B4750-2A48-4D19-9404-178AAB946482} = {E76226D2-994C-4EE1-B346-050F31B175BD} {A0611EAD-FB04-44E7-BAD3-459DD0A7FF46} = {E76226D2-994C-4EE1-B346-050F31B175BD} + {4BC7B119-BF3F-4FED-84BD-5909543343D5} = {E76226D2-994C-4EE1-B346-050F31B175BD} + {1CDBEB80-6DDF-494B-9A26-47E889E523A9} = {E76226D2-994C-4EE1-B346-050F31B175BD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {262CE437-AD82-4481-8B77-593288986C70} diff --git a/tests/McpEndToEndTest/McpEndToEndTest.nfproj b/tests/McpEndToEndTest/McpEndToEndTest.nfproj new file mode 100644 index 0000000..982932e --- /dev/null +++ b/tests/McpEndToEndTest/McpEndToEndTest.nfproj @@ -0,0 +1,66 @@ + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 1cdbeb80-6ddf-494b-9a26-47e889e523a9 + Exe + Properties + 512 + McpEndToEndTest + McpEndToEndTest + v1.0 + + + + + + + + + + ..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll + + + ..\..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll + + + ..\..\packages\nanoFramework.System.Collections.1.5.67\lib\nanoFramework.System.Collections.dll + + + ..\..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\..\packages\nanoFramework.System.Net.1.11.43\lib\System.Net.dll + + + ..\..\packages\nanoFramework.System.Net.Http.Server.1.5.196\lib\System.Net.Http.dll + + + ..\..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/McpEndToEndTest/McpToolsClasses.cs b/tests/McpEndToEndTest/McpToolsClasses.cs new file mode 100644 index 0000000..3efcb39 --- /dev/null +++ b/tests/McpEndToEndTest/McpToolsClasses.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using nanoFramework.WebServer.Mcp; + +namespace McpServerTests +{ + public class Person + { + public string Name { get; set; } + public string Surname { get; set; } + public int Age { get; set; } = 30; // Default age + public Address Address { get; set; } = new Address(); // Default address + } + + public class Address + { + public string Street { get; set; } = "Unknown"; + public string City { get; set; } = "Unknown"; + public string PostalCode { get; set; } = "00000"; + public string Country { get; set; } = "Unknown"; + } + + public class McpTools + { + [McpTool("process_person", "Processes a person object.", null, null, typeof(Person))] + public static string ProcessPerson(Person person) + { + return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; + } + } +} diff --git a/tests/McpEndToEndTest/Program.cs b/tests/McpEndToEndTest/Program.cs new file mode 100644 index 0000000..22783b9 --- /dev/null +++ b/tests/McpEndToEndTest/Program.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; +//using nanoFramework.WebServer.Mcp; + +namespace McpEndToEndTest +{ + public class Program + { + public static void Main() + { + Debug.WriteLine("Hello from MCP Server!"); + + //McpToolRegistry.DiscoverTools(new Type[] { typeof(McpServerTests.McpTools) }); + + Thread.Sleep(Timeout.Infinite); + } + } +} diff --git a/tests/McpEndToEndTest/Properties/AssemblyInfo.cs b/tests/McpEndToEndTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d459bd3 --- /dev/null +++ b/tests/McpEndToEndTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CSharp.BlankApplication")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CSharp.BlankApplication")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/McpEndToEndTest/packages.config b/tests/McpEndToEndTest/packages.config new file mode 100644 index 0000000..958c4f3 --- /dev/null +++ b/tests/McpEndToEndTest/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/McpEndToEndTest/packages.lock.json b/tests/McpEndToEndTest/packages.lock.json new file mode 100644 index 0000000..ddd01f9 --- /dev/null +++ b/tests/McpEndToEndTest/packages.lock.json @@ -0,0 +1,61 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.Json": { + "type": "Direct", + "requested": "[2.2.199, 2.2.199]", + "resolved": "2.2.199", + "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w==" + }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.32, 1.11.32]", + "resolved": "1.11.32", + "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA==" + }, + "nanoFramework.System.Collections": { + "type": "Direct", + "requested": "[1.5.67, 1.5.67]", + "resolved": "1.5.67", + "contentHash": "MjSipUB70vrxjqTm1KfKTUqqjd0wbweiNyYFXONi0XClrH6HXsuX2lhDqXM8NWuYnWyYOqx8y20sXbvsH+4brg==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.43, 1.11.43]", + "resolved": "1.11.43", + "contentHash": "USwz59gxcNUzsiXfQohWSi8ANNwGDsp+qG4zBtHZU3rKMtvTsLI3rxdfMC77VehKqsCPn7aK3PU2oCRFo+1Rgg==" + }, + "nanoFramework.System.Net.Http.Server": { + "type": "Direct", + "requested": "[1.5.196, 1.5.196]", + "resolved": "1.5.196", + "contentHash": "cjr5Rj39duOjGcyvo/LMFdoeTeLg0zpFgFB7wJUXw0+65EiENEnJwqqR1CfbJEvBBpBMJdH/yLkK/8DU8Jk3XQ==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + } + } + } +} \ No newline at end of file diff --git a/tests/McpServerTests/McpServerTests.nfproj b/tests/McpServerTests/McpServerTests.nfproj new file mode 100644 index 0000000..268c631 --- /dev/null +++ b/tests/McpServerTests/McpServerTests.nfproj @@ -0,0 +1,54 @@ + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4bc7b119-bf3f-4fed-84bd-5909543343d5 + Library + Properties + 512 + McpServerTests + NFUnitTest + False + true + UnitTest + v1.0 + + + + $(MSBuildProjectDirectory)\nano.runsettings + + + + + + + + + ..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.TestFramework.dll + + + ..\..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.UnitTestLauncher.exe + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/McpServerTests/McpToolsAttributeTests.cs b/tests/McpServerTests/McpToolsAttributeTests.cs new file mode 100644 index 0000000..84a84bc --- /dev/null +++ b/tests/McpServerTests/McpToolsAttributeTests.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using nanoFramework.TestFramework; + +namespace McpServerTests +{ + [TestClass] + public class TestMcpToolsAttributeTests + { + [TestMethod] + public void TestSimpleTypeString() + { + // Arrange + Type inputType = typeof(string); + string expectedJson = "[{\"name\":\"value\",\"type\":\"string\",\"description\":\"value\"}]"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple string type does not match the expected output."); + } + } +} diff --git a/tests/McpServerTests/McpToolsClasses.cs b/tests/McpServerTests/McpToolsClasses.cs new file mode 100644 index 0000000..3efcb39 --- /dev/null +++ b/tests/McpServerTests/McpToolsClasses.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using nanoFramework.WebServer.Mcp; + +namespace McpServerTests +{ + public class Person + { + public string Name { get; set; } + public string Surname { get; set; } + public int Age { get; set; } = 30; // Default age + public Address Address { get; set; } = new Address(); // Default address + } + + public class Address + { + public string Street { get; set; } = "Unknown"; + public string City { get; set; } = "Unknown"; + public string PostalCode { get; set; } = "00000"; + public string Country { get; set; } = "Unknown"; + } + + public class McpTools + { + [McpTool("process_person", "Processes a person object.", null, null, typeof(Person))] + public static string ProcessPerson(Person person) + { + return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; + } + } +} diff --git a/tests/McpServerTests/Properties/AssemblyInfo.cs b/tests/McpServerTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..97c660c --- /dev/null +++ b/tests/McpServerTests/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright (c) 2021 nanoFramework contributors")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/McpServerTests/nano.runsettings b/tests/McpServerTests/nano.runsettings new file mode 100644 index 0000000..93ce85e --- /dev/null +++ b/tests/McpServerTests/nano.runsettings @@ -0,0 +1,17 @@ + + + + + .\TestResults + 120000 + net48 + x64 + + + None + False + COM3 + + + + \ No newline at end of file diff --git a/tests/McpServerTests/packages.config b/tests/McpServerTests/packages.config new file mode 100644 index 0000000..28d330b --- /dev/null +++ b/tests/McpServerTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/McpServerTests/packages.lock.json b/tests/McpServerTests/packages.lock.json new file mode 100644 index 0000000..2137220 --- /dev/null +++ b/tests/McpServerTests/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.TestFramework": { + "type": "Direct", + "requested": "[3.0.77, 3.0.77]", + "resolved": "3.0.77", + "contentHash": "Py5W1oN84KMBmOOHCzdz6pyi3bZTnQu9BoqIx0KGqkhG3V8kGoem/t+BuCM0pMIWAyl2iMP1n2S9624YXmBJZw==" + } + } + } +} \ No newline at end of file From 9b4ea969c1497afa8bb622cf54ba9e8552e0a115 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Tue, 24 Jun 2025 10:08:46 +0200 Subject: [PATCH 2/8] First implementation --- .devcontainer/devcontainer.json | 17 + .gitignore | 2 + .../DescriptionAttribute.cs | 27 ++ .../HashtableExtension.cs | 24 ++ .../McpServerBasicAuthenticationController.cs | 14 + .../McpServerController.cs | 158 ++++++++++ .../McpServerKeyAuthenticationController.cs | 14 + .../McpServerToolAttribute.cs | 45 +++ .../McpToolAttribute.cs | 39 --- .../McpToolRegistry.cs | 296 +++++++++++++++++- .../McpToolsJsonHelper.cs | 125 +++++++- nanoFramework.WebServer.Mcp/ToolMetadata.cs | 40 +++ .../nanoFramework.WebServer.Mcp.nfproj | 25 +- nanoFramework.WebServer.Mcp/packages.config | 5 + .../packages.lock.json | 30 ++ tests/McpClientTest/McpClientTest.cs | 82 +++++ tests/McpEndToEndTest/McpEndToEndTest.nfproj | 8 + tests/McpEndToEndTest/McpToolsClasses.cs | 43 ++- tests/McpEndToEndTest/Program.cs | 43 ++- tests/McpEndToEndTest/packages.config | 1 + tests/McpEndToEndTest/packages.lock.json | 6 + tests/McpEndToEndTest/requests.http | 38 +++ tests/McpServerTests/McpToolsClasses.cs | 2 +- 23 files changed, 1015 insertions(+), 69 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 nanoFramework.WebServer.Mcp/DescriptionAttribute.cs create mode 100644 nanoFramework.WebServer.Mcp/HashtableExtension.cs create mode 100644 nanoFramework.WebServer.Mcp/McpServerBasicAuthenticationController.cs create mode 100644 nanoFramework.WebServer.Mcp/McpServerController.cs create mode 100644 nanoFramework.WebServer.Mcp/McpServerKeyAuthenticationController.cs create mode 100644 nanoFramework.WebServer.Mcp/McpServerToolAttribute.cs delete mode 100644 nanoFramework.WebServer.Mcp/McpToolAttribute.cs create mode 100644 tests/McpClientTest/McpClientTest.cs create mode 100644 tests/McpEndToEndTest/requests.http diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..afe971c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "C# .NET 10 Preview", + "image": "mcr.microsoft.com/devcontainers/dotnet:dev-10.0-preview-noble", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "editorconfig.editorconfig", + "GitHub.copilot-chat" + ] + } + }, + "postCreateCommand": "dotnet --version" +} diff --git a/.gitignore b/.gitignore index b799600..c8a7c0e 100644 --- a/.gitignore +++ b/.gitignore @@ -258,3 +258,5 @@ paket-files/ #VSCode .vscode +tests/McpEndToEndTest/WiFi.cs +**/.env \ No newline at end of file diff --git a/nanoFramework.WebServer.Mcp/DescriptionAttribute.cs b/nanoFramework.WebServer.Mcp/DescriptionAttribute.cs new file mode 100644 index 0000000..49dcbc2 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/DescriptionAttribute.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace nanoFramework.WebServer.Mcp +{ + /// + /// Specifies a description for a class member, such as a property or method, for use in documentation or metadata. + /// + public class DescriptionAttribute : Attribute + { + /// + /// Gets the description text associated with the member. + /// + public string Description { get; } + + /// + /// Initializes a new instance of the class with the specified description. + /// + /// The description text to associate with the member. + public DescriptionAttribute(string description) + { + Description = description; + } + } +} diff --git a/nanoFramework.WebServer.Mcp/HashtableExtension.cs b/nanoFramework.WebServer.Mcp/HashtableExtension.cs new file mode 100644 index 0000000..bbde9f4 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/HashtableExtension.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using System.Collections; + +namespace nanoFramework.WebServer.Mcp +{ + internal static class HashtableExtension + { + public static bool ContainsKey(this Hashtable hashtable, string key) + { + foreach (object k in hashtable.Keys) + { + if (k is string strKey && strKey.Equals(key)) + { + return true; + } + } + + return false; + } + } +} diff --git a/nanoFramework.WebServer.Mcp/McpServerBasicAuthenticationController.cs b/nanoFramework.WebServer.Mcp/McpServerBasicAuthenticationController.cs new file mode 100644 index 0000000..bd3e5ce --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpServerBasicAuthenticationController.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace nanoFramework.WebServer.Mcp +{ + /// + /// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools. + /// This controller is specifically designed for basic (user, password) authentication. + /// + [Authentication("Basic")] + public class McpServerBasicAuthenticationController : McpServerController + { + } +} diff --git a/nanoFramework.WebServer.Mcp/McpServerController.cs b/nanoFramework.WebServer.Mcp/McpServerController.cs new file mode 100644 index 0000000..84ef172 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpServerController.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Text; +using nanoFramework.Json; + +namespace nanoFramework.WebServer.Mcp +{ + /// + /// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools. + /// + public class McpServerController + { + /// + /// Handles POST requests to the "mcp" route. + /// Processes the incoming request, invokes the specified tool with provided parameters, and writes the result to the response stream in JSON format. + /// + /// The web server event arguments containing the HTTP context and request/response information. + [Route("mcp"), Method("POST")] + public void HandleMcpRequest(WebServerEventArgs e) + { + e.Context.Response.ContentType = "application/json"; + int id = 0; + StringBuilder sb = new StringBuilder(); + + try + { + // Read the POST body from the request stream + var requestStream = e.Context.Request.InputStream; + byte[] buffer = new byte[requestStream.Length]; + requestStream.Read(buffer, 0, buffer.Length); + string requestBody = Encoding.UTF8.GetString(buffer, 0, buffer.Length); + + Debug.WriteLine($"Request Body: {requestBody}"); + + Hashtable request = (Hashtable)JsonConvert.DeserializeObject(requestBody, typeof(Hashtable)); + +//#if DEBUG +// foreach (string key in request.Keys) +// { +// Debug.WriteLine($"Key: {key}, Value: {request[key]}"); +// } +//#endif + + // Sets jsonrpc version + sb.Append("{\"jsonrpc\": \"2.0\""); + // Check if we have an id if yes, add it to the answer + if (request.ContainsKey("id")) + { + id = Convert.ToInt32(request["id"].ToString()); + sb.Append($",\"id\":{id}"); + } + + if (request.ContainsKey("method")) + { + // Case the server us initilaized + if (request["method"].ToString() == "notifications/initialized") + { + WebServer.OutputHttpCode(e.Context.Response, System.Net.HttpStatusCode.OK); + return; + } + + if (request["method"].ToString() == "initialize") + { + // TODO: check the received version and adjust with error message or set this version + sb.Append(",\"result\":{\"protocolVersion\":\"2025-03-26\""); + + // Add capabilities + sb.Append($",\"capabilities\":{{\"logging\":{{}},\"prompts\":{{\"listChanged\":false}},\"resources\":{{\"subscribe\":false,\"listChanged\":false}},\"tools\":{{\"listChanged\":false}}}}"); + + // Add serverInfo + sb.Append($",\"serverInfo\":{{\"name\":\"nanoFramework\",\"version\":\"1.0.0\"}}"); + + // Add instructions + sb.Append($",\"instructions\":\"This is an embedded device and only 1 request at a time should be sent. Make sure you pass numbers as numbers and not as string.\"}}}}"); + //sb.Append("\r\n\r\n{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}"); + WebServer.OutPutStream(e.Context.Response, sb.ToString()); + return; + } + + if (request["method"].ToString() == "tools/list") + { + // This is a request for the list of tools + string toolListJson = McpToolRegistry.GetToolMetadataJson(); + sb.Append($",\"result\":{{{toolListJson}}}}}"); + WebServer.OutPutStream(e.Context.Response, sb.ToString()); + return; + } + + // Check if the method starts with "tools/call" and extract the method name to call the tool + if (request["method"].ToString() == "tools/call") + { + //string param = JsonConvert.SerializeObject(((Hashtable)request["params"])["arguments"]); + string toolName = ((Hashtable)request["params"])["name"].ToString(); + Hashtable param = ((Hashtable)request["params"])["arguments"] == null ? null : (Hashtable)((Hashtable)request["params"])["arguments"]; + + string result = McpToolRegistry.InvokeTool(toolName, param); + + //Debug.WriteLine($"Tool: {toolName}, Param: {param}"); + + sb.Append($",\"result\":{{\"content\":[{{\"type\":\"text\",\"text\":{JsonConvert.SerializeObject(result)}}}]}}}}"); + WebServer.OutPutStream(e.Context.Response, sb.ToString()); + return; + } + else + { + sb.Append($",\"error\":{{\"code\":-32601,\"message\":\"Method not found\"}}}}"); + WebServer.OutPutStream(e.Context.Response, sb.ToString()); + return; + } + } + } + catch (Exception ex) + { + WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"id\":{id},\"error\":{{\"code\":-32602,\"message\":\"{ex.Message}\"}}}}"); + } + } + + /// + /// Handles GET requests to the "tools" route. + /// Returns a JSON list of all available tools and their metadata. + /// + /// The web server event arguments containing the HTTP context and request/response information. + [Route("tools"), Method("GET")] + public void GetToolList(WebServerEventArgs e) + { + e.Context.Response.ContentType = "application/json"; + WebServer.OutputHttpCode(e.Context.Response, System.Net.HttpStatusCode.NotFound); + return; + + try + { + Debug.WriteLine($"GET request for tool list received: {e.Context.Request.RawUrl}"); + string toolListJson = McpToolRegistry.GetToolMetadataJson(); + WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"result\":{{{toolListJson}}}}}"); + } + catch (Exception ex) + { + WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"{ex.Message}\"}}}}"); + } + + // Free up the memory used + Runtime.Native.GC.Run(true); + } + + /// + /// Handles GET requests to the "mcp" route. + /// Returns the same tool list as . + /// + /// The web server event arguments containing the HTTP context and request/response information. + [Route("mcp"), Method("GET")] + public void GetToolMcpList(WebServerEventArgs e) => GetToolList(e); + } +} diff --git a/nanoFramework.WebServer.Mcp/McpServerKeyAuthenticationController.cs b/nanoFramework.WebServer.Mcp/McpServerKeyAuthenticationController.cs new file mode 100644 index 0000000..a933b63 --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpServerKeyAuthenticationController.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace nanoFramework.WebServer.Mcp +{ + /// + /// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools. + /// This controller is specifically designed for key-based authentication. + /// + [Authentication("ApiKey")] + public class McpServerKeyAuthenticationController : McpServerController + { + } +} diff --git a/nanoFramework.WebServer.Mcp/McpServerToolAttribute.cs b/nanoFramework.WebServer.Mcp/McpServerToolAttribute.cs new file mode 100644 index 0000000..414010d --- /dev/null +++ b/nanoFramework.WebServer.Mcp/McpServerToolAttribute.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Reflection; +using System.Text; + +namespace nanoFramework.WebServer.Mcp +{ + /// + /// Attribute to mark a method as an MCP server tool and provide metadata for discovery and documentation. + /// + [AttributeUsage(AttributeTargets.Method)] + public class McpServerToolAttribute : Attribute + { + /// + /// Gets the unique name of the tool. + /// + public string Name { get; } + + /// + /// Gets the description of the tool. + /// + public string Description { get; } + + /// + /// Gets the description of the tool's output. + /// + public string OutputDescription { get; } + + /// + /// Initializes a new instance of the class with the specified name, description, and output description. + /// + /// The unique name of the tool. + /// The description of the tool. + /// The description of the tool's output. + public McpServerToolAttribute(string name, string description = "", string outputDescription = "") + { + Name = name; + Description = description; + OutputDescription = outputDescription; + } + } +} diff --git a/nanoFramework.WebServer.Mcp/McpToolAttribute.cs b/nanoFramework.WebServer.Mcp/McpToolAttribute.cs deleted file mode 100644 index 73b04a3..0000000 --- a/nanoFramework.WebServer.Mcp/McpToolAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections; -using System.Reflection; -using System.Text; - -namespace nanoFramework.WebServer.Mcp -{ - [AttributeUsage(AttributeTargets.Method)] - public class McpToolAttribute : Attribute - { - public string Name { get; } - public string Description { get; } - public string InputType { get; internal set; } - public string OutputType { get; internal set; } - public Type DefaultValue { get; } - - public McpToolAttribute(string name, string description, string inputType = null, string outputType = null, Type defaultValue = null) - { - Name = name; - Description = description; - InputType = inputType; - OutputType = outputType; - DefaultValue = defaultValue; - } - - public void SetInputType(object inputType) - { - InputType = McpToolJsonHelper.GenerateInputJson(inputType.GetType()); - } - - public void SetOutputType(string outputType) - { - OutputType = McpToolJsonHelper.GenerateInputJson(outputType.GetType()); - } - } -} diff --git a/nanoFramework.WebServer.Mcp/McpToolRegistry.cs b/nanoFramework.WebServer.Mcp/McpToolRegistry.cs index 39c9b88..af0949e 100644 --- a/nanoFramework.WebServer.Mcp/McpToolRegistry.cs +++ b/nanoFramework.WebServer.Mcp/McpToolRegistry.cs @@ -6,41 +6,317 @@ using System.Reflection; using nanoFramework.Json; using System.Threading; +using System.Text; +using System.Diagnostics; namespace nanoFramework.WebServer.Mcp { + /// + /// Registry for Model Context Protocol (MCP) tools, allowing discovery and invocation of tools defined with the McpServerToolAttribute. + /// public static class McpToolRegistry { private static readonly Hashtable tools = new Hashtable(); + private static bool isInitialized = false; + /// + /// Discovers MCP tools by scanning the provided types for methods decorated with the McpServerToolAttribute. + /// This method should be called once to populate the tool registry. + /// + /// An array of types to scan for MCP tools. public static void DiscoverTools(Type[] mcpTools) { + if (isInitialized) + { + return; // Tools already discovered + } + foreach (Type mcpTool in mcpTools) { - MethodInfo[] methods = mcpTool.GetMethods(BindingFlags.Public | BindingFlags.Static); + MethodInfo[] methods = mcpTool.GetMethods(); foreach (MethodInfo method in methods) { - var allAttribute = method.GetCustomAttributes(false); - foreach (var attrib in allAttribute) + try { - if (attrib.GetType() == typeof(McpToolAttribute)) + var allAttribute = method.GetCustomAttributes(true); + foreach (var attrib in allAttribute) { - McpToolAttribute attribute = (McpToolAttribute)attrib; + if (attrib.GetType() != typeof(McpServerToolAttribute)) + { + continue; + } + + McpServerToolAttribute attribute = (McpServerToolAttribute)attrib; if (attribute != null) { - tools[attribute.Name] = new ToolMetadata + var parameters = method.GetParameters(); + string inputType = string.Empty; + if (parameters.Length > 0) + { + inputType = McpToolJsonHelper.GenerateInputJson(parameters[0].ParameterType); + } + + tools.Add(attribute.Name, new ToolMetadata { Name = attribute.Name, Description = attribute.Description, - InputType = attribute.InputType, - OutputType = attribute.OutputType, + InputType = inputType, + OutputType = McpToolJsonHelper.GenerateOutputJson(method.ReturnType, attribute.OutputDescription), Method = method - }; + }); } } } + catch (Exception) + { + continue; + } } } - } + + isInitialized = true; + } + + /// + /// Gets the metadata of all registered MCP tools in JSON format. + /// This method should be called after DiscoverTools to retrieve the tool metadata. + /// + /// A JSON string containing the metadata of all registered tools. + /// Thrown if there is an error building the tools list. + public static string GetToolMetadataJson() + { + try + { + StringBuilder sb = new StringBuilder(); + sb.Append("\"tools\":["); + + foreach (ToolMetadata tool in tools.Values) + { + sb.Append(tool.ToString()); + sb.Append(","); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append("],\"nextCursor\":null"); + return sb.ToString(); + + } + catch (Exception) + { + throw new Exception("Impossible to build tools list."); + } + } + + private static bool IsPrimitiveType(Type type) + { + return type == typeof(bool) || + type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(char) || + type == typeof(double) || + type == typeof(float) || + type == typeof(int) || + type == typeof(uint) || + type == typeof(long) || + type == typeof(ulong) || + type == typeof(short) || + type == typeof(ushort); + } + + private static object CreateInstance(Type type) + { + // Get the default constructor + ConstructorInfo constructor = type.GetConstructor(new Type[0]); + if (constructor == null) + { + throw new Exception($"Type {type.Name} does not have a parameterless constructor"); + } + + return constructor.Invoke(new object[0]); + } + + /// + /// Recursively deserializes a Hashtable into a strongly-typed object by mapping properties and handling nested objects. + /// + /// The Hashtable containing the data to deserialize. + /// The target type to deserialize the data into. + /// A new instance of the target type with properties populated from the Hashtable, or null if hashtable or targetType is null. + private static object DeserializeFromHashtable(Hashtable hashtable, Type targetType) + { + if (hashtable == null || targetType == null) + { + return null; + } + + // For primitive types and strings, try direct conversion + if (IsPrimitiveType(targetType) || targetType == typeof(string)) + { + // This shouldn't happen in our context, but handle it gracefully + return hashtable; + } + + // Create instance of the target type + object instance = CreateInstance(targetType); + + // Get all methods of the target type + MethodInfo[] methods = targetType.GetMethods(); + + // Find setter methods (set_PropertyName) + foreach (MethodInfo method in methods) + { + if (!method.Name.StartsWith("set_") || method.GetParameters().Length != 1) + { + continue; + } + + // Extract property name from setter method name + string propertyName = method.Name.Substring(4); // Remove "set_" prefix + + // Check if the hashtable contains this property + if (!hashtable.Contains(propertyName)) + { + continue; + } + + object value = hashtable[propertyName]; + if (value == null) + { + continue; + } + + try + { + // Get the parameter type of the setter method (which is the property type) + Type propertyType = method.GetParameters()[0].ParameterType; + + // Handle primitive types and strings + if (IsPrimitiveType(propertyType) || propertyType == typeof(string)) + { + // Direct assignment for primitive types and strings + if (propertyType == typeof(string)) + { + method.Invoke(instance, new object[] { value.ToString() }); + } + else if (propertyType == typeof(int)) + { + method.Invoke(instance, new object[] { Convert.ToInt32(value.ToString()) }); + } + else if (propertyType == typeof(double)) + { + method.Invoke(instance, new object[] { Convert.ToDouble(value.ToString()) }); + } + else if (propertyType == typeof(bool)) + { + try + { + method.Invoke(instance, new object[] { Convert.ToBoolean(Convert.ToByte(value.ToString())) }); + } + catch (Exception) + { + method.Invoke(instance, new object[] { value.ToString().ToLower() == "true" ? true : false }); + } + } + else if (propertyType == typeof(long)) + { + method.Invoke(instance, new object[] { Convert.ToInt64(value.ToString()) }); + } + else if (propertyType == typeof(float)) + { + method.Invoke(instance, new object[] { Convert.ToSingle(value.ToString()) }); + } + else if (propertyType == typeof(byte)) + { + method.Invoke(instance, new object[] { Convert.ToByte(value.ToString()) }); + } + else if (propertyType == typeof(short)) + { + method.Invoke(instance, new object[] { Convert.ToInt16(value.ToString()) }); + } + else if (propertyType == typeof(char)) + { + try + { + method.Invoke(instance, new object[] { Convert.ToChar(Convert.ToUInt16(value.ToString())) }); + } + catch (Exception) + { + method.Invoke(instance, new object[] { string.IsNullOrEmpty(value.ToString()) ? '\0' : value.ToString()[0] }); + } + } + } + else + { + // Handle complex types (nested objects) + if (value is string stringValue) + { + // The nested object is serialized as a JSON string + var nestedHashtable = (Hashtable)JsonConvert.DeserializeObject(stringValue, typeof(Hashtable)); + object nestedObject = DeserializeFromHashtable(nestedHashtable, propertyType); + method.Invoke(instance, new object[] { nestedObject }); + } + else if (value is Hashtable nestedHashtable) + { + // The nested object is already a Hashtable + object nestedObject = DeserializeFromHashtable(nestedHashtable, propertyType); + method.Invoke(instance, new object[] { nestedObject }); + } + } + } + catch (Exception) + { + // Skip properties that can't be set + continue; + } + } + + return instance; + } + + /// + /// Invokes a registered MCP tool by name with the specified parameters and returns the serialized result. + /// + /// The name of the tool to invoke. + /// The parameters to pass to the tool as a Hashtable. + /// A JSON string containing the serialized result of the tool invocation. + /// Thrown when the specified tool is not found in the registry. + public static string InvokeTool(string toolName, Hashtable parameter) + { + if (tools.Contains(toolName)) + { + ToolMetadata toolMetadata = (ToolMetadata)tools[toolName]; + MethodInfo method = toolMetadata.Method; + Debug.WriteLine($"Tool name: {toolName}, method: {method.Name}"); + ParameterInfo[] parametersInfo = method.GetParameters(); +#if DEBUG + foreach(ParameterInfo parameterInfo in parametersInfo) + { + Debug.WriteLine($"method type: {parameterInfo.ParameterType.FullName}"); + } +#endif + + object[] methodParams = null; + if (parametersInfo.Length > 0) + { + methodParams = new object[parametersInfo.Length]; + Type paramType = parametersInfo[0].ParameterType; + + if (IsPrimitiveType(paramType) || paramType == typeof(string)) + { + // For primitive types, use direct assignment + methodParams[0] = parameter; + } + else + { + // For complex types, use our recursive deserialization + methodParams[0] = DeserializeFromHashtable(parameter, paramType); + } + } + + object result = method.Invoke(null, methodParams); + return JsonConvert.SerializeObject(result); + } + + throw new Exception("Tool not found"); + } } } diff --git a/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs b/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs index d8caf1f..bcf15e3 100644 --- a/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs +++ b/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs @@ -7,43 +7,144 @@ namespace nanoFramework.WebServer.Mcp { + /// + /// Provides utility methods for generating JSON schemas that describe the input and output parameters of tools. + /// + /// This class includes methods for creating JSON representations of input and output schemas, + /// including parameter names, types, and descriptions. It is designed to assist in dynamically generating metadata + /// for tools or APIs. public static class McpToolJsonHelper { + /// + /// Generates a JSON array describing the input parameters for a tool, including their names, types, and descriptions. + /// + /// An array of objects representing the input parameter types. + /// A JSON string representing the input parameters schema. public static string GenerateInputJson(Type inputType) { StringBuilder sb = new StringBuilder(); - sb.Append("["); - AppendPropertiesJson(sb, inputType, true); - sb.Append("]"); + sb.Append("{\"type\":\"object\",\"properties\":{"); + AppendInputPropertiesJson(sb, inputType, true); + sb.Append("},\"required\":[]}"); return sb.ToString(); } - private static void AppendPropertiesJson(StringBuilder sb, Type type, bool isFirst) + /// + /// Generates a JSON object describing the output schema for a tool, including type, description, and nested properties if applicable. + /// + /// The of the output object. + /// A description of the output. + /// A JSON string representing the output schema. + public static string GenerateOutputJson(Type outputType, string description) + { + StringBuilder sb = new StringBuilder(); + AppendOutputJson(sb, outputType, description); + return sb.ToString(); + } + + private static void AppendOutputJson(StringBuilder sb, Type type, string description) + { + string mappedType = MapType(type); + + sb.Append("{"); + sb.Append("\"type\":\"").Append(mappedType).Append("\""); + + bool hasDescription = !string.IsNullOrEmpty(description); + if (hasDescription) + { + sb.Append(",\"description\":\"").Append(description).Append("\""); + } + + if (mappedType == "object") + { + sb.Append(",\"properties\":{"); + AppendOutputPropertiesJson(sb, type, true); + sb.Append("}"); + } + sb.Append("}"); + } + + private static void AppendOutputPropertiesJson(StringBuilder sb, Type type, bool isFirst) { MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); - // Find all property names by looking for get_ methods for (int i = 0; i < methods.Length; i++) { MethodInfo method = methods[i]; if (method.Name.StartsWith("get_") && method.GetParameters().Length == 0) { string propName = method.Name.Substring(4); + Type propType = method.ReturnType; + string mappedType = MapType(propType); + + if (!isFirst) + { + sb.Append(","); + } - if (!isFirst) sb.Append(","); isFirst = false; - sb.Append("{"); - sb.Append("\"name\":\"").Append(propName).Append("\","); + sb.Append("\"").Append(propName).Append("\":"); + if (mappedType == "object") + { + AppendOutputJson(sb, propType, GetTypeDescription(method, propName)); + } + else + { + sb.Append("{"); + sb.Append("\"type\":\"").Append(mappedType).Append("\","); + sb.Append("\"description\":\"").Append(GetTypeDescription(method, propName)).Append("\""); + sb.Append("}"); + } + } + } + } + + private static string GetTypeDescription(MethodInfo method, string propName) + { + var atibs = method.GetCustomAttributes(false); + string desc = propName; + for (int j = 0; j < atibs.Length; j++) + { + if (atibs[j] is DescriptionAttribute descAttrib) + { + desc = descAttrib.Description; + break; + } + } + + return desc; + } + + private static void AppendInputPropertiesJson(StringBuilder sb, Type type, bool isFirst) + { + MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); + + for (int i = 0; i < methods.Length; i++) + { + MethodInfo method = methods[i]; + if (method.Name.StartsWith("get_") && method.GetParameters().Length == 0) + { + string propName = method.Name.Substring(4); + Type propType = method.ReturnType; + + if (!isFirst) + { + sb.Append(","); + } + + isFirst = false; + sb.Append($"\"{propName}\":{{"); string mappedType = MapType(propType); sb.Append("\"type\":\"").Append(mappedType).Append("\","); - sb.Append("\"description\":\"").Append(propName).Append("\""); + sb.Append("\"description\":\"").Append(GetTypeDescription(method, propName)).Append("\""); if (mappedType == "object") { - sb.Append(",\"properties\":["); - AppendPropertiesJson(sb, propType, true); - sb.Append("]"); + sb.Append(",\"properties\":{"); + AppendInputPropertiesJson(sb, propType, true); + sb.Append("}"); } + sb.Append("}"); } } diff --git a/nanoFramework.WebServer.Mcp/ToolMetadata.cs b/nanoFramework.WebServer.Mcp/ToolMetadata.cs index 46e11cb..9a4f6e8 100644 --- a/nanoFramework.WebServer.Mcp/ToolMetadata.cs +++ b/nanoFramework.WebServer.Mcp/ToolMetadata.cs @@ -6,12 +6,52 @@ namespace nanoFramework.WebServer.Mcp { + /// + /// Represents metadata information for a registered tool, including its name, description, input/output types, and associated method. + /// public class ToolMetadata { + /// + /// Gets or sets the unique name of the tool. + /// public string Name { get; set; } + + /// + /// Gets or sets the description of the tool. + /// public string Description { get; set; } + + /// + /// Gets or sets the description of the tool's output. + /// + public string OutputDescription { get; set; } + + /// + /// Gets or sets the JSON schema string describing the input parameters for the tool. + /// public string InputType { get; set; } + + /// + /// Gets or sets the JSON schema string describing the output type for the tool. + /// public string OutputType { get; set; } + + /// + /// Gets or sets the representing the method associated with the tool. + /// public MethodInfo Method { get; set; } + + /// + /// Returns a JSON string representation of the tool metadata. + /// + /// A JSON string containing the tool's name, description, input, and output schema. + public override string ToString() + { + string output = $"{{\"name\":\"{Name}\",\"description\":\"{Description}\""; + output += string.IsNullOrEmpty(InputType) ? string.Empty : $",\"inputSchema\":{InputType}"; + output += string.IsNullOrEmpty(OutputType) ? string.Empty : $",\"outputSchema\":{OutputType}"; + output += "}"; + return output; + } } } diff --git a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj index 3ae8897..ef9e4f0 100644 --- a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj +++ b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj @@ -18,9 +18,14 @@ - + + + + + + @@ -31,6 +36,12 @@ ..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll + + ..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll + + + ..\packages\nanoFramework.Runtime.Native.1.7.11\lib\nanoFramework.Runtime.Native.dll + ..\packages\nanoFramework.System.Collections.1.5.67\lib\nanoFramework.System.Collections.dll @@ -40,6 +51,15 @@ ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + ..\packages\nanoFramework.System.Net.1.11.43\lib\System.Net.dll + + + ..\packages\nanoFramework.System.Net.Http.Server.1.5.196\lib\System.Net.Http.dll + + + ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + @@ -47,6 +67,9 @@ + + + diff --git a/nanoFramework.WebServer.Mcp/packages.config b/nanoFramework.WebServer.Mcp/packages.config index ea1e821..82b7f1d 100644 --- a/nanoFramework.WebServer.Mcp/packages.config +++ b/nanoFramework.WebServer.Mcp/packages.config @@ -2,8 +2,13 @@ + + + + + \ No newline at end of file diff --git a/nanoFramework.WebServer.Mcp/packages.lock.json b/nanoFramework.WebServer.Mcp/packages.lock.json index df144f1..3480499 100644 --- a/nanoFramework.WebServer.Mcp/packages.lock.json +++ b/nanoFramework.WebServer.Mcp/packages.lock.json @@ -14,6 +14,18 @@ "resolved": "2.2.199", "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w==" }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.32, 1.11.32]", + "resolved": "1.11.32", + "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA==" + }, + "nanoFramework.Runtime.Native": { + "type": "Direct", + "requested": "[1.7.11, 1.7.11]", + "resolved": "1.7.11", + "contentHash": "XPSTltZ9KeBruogVmjQpCphi1nLoJH49mpyp2eGBs8BTjKuL5TkMO20MoI8r73F/PW5AppTq49HvIZZavU5nPQ==" + }, "nanoFramework.System.Collections": { "type": "Direct", "requested": "[1.5.67, 1.5.67]", @@ -26,12 +38,30 @@ "resolved": "1.1.96", "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.43, 1.11.43]", + "resolved": "1.11.43", + "contentHash": "USwz59gxcNUzsiXfQohWSi8ANNwGDsp+qG4zBtHZU3rKMtvTsLI3rxdfMC77VehKqsCPn7aK3PU2oCRFo+1Rgg==" + }, + "nanoFramework.System.Net.Http.Server": { + "type": "Direct", + "requested": "[1.5.196, 1.5.196]", + "resolved": "1.5.196", + "contentHash": "cjr5Rj39duOjGcyvo/LMFdoeTeLg0zpFgFB7wJUXw0+65EiENEnJwqqR1CfbJEvBBpBMJdH/yLkK/8DU8Jk3XQ==" + }, "nanoFramework.System.Text": { "type": "Direct", "requested": "[1.3.42, 1.3.42]", "resolved": "1.3.42", "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, "Nerdbank.GitVersioning": { "type": "Direct", "requested": "[3.7.115, 3.7.115]", diff --git a/tests/McpClientTest/McpClientTest.cs b/tests/McpClientTest/McpClientTest.cs new file mode 100644 index 0000000..aeb61d0 --- /dev/null +++ b/tests/McpClientTest/McpClientTest.cs @@ -0,0 +1,82 @@ +#!/usr/bin/dotnet run + +#:package DotNetEnv@3.1.1 +#:package ModelContextProtocol@0.2.0-preview.3 +#:package Microsoft.SemanticKernel@1.49.0 + +// Note: this is .NET single file. Run with: dotnet run McpClientTest.cs + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using ModelContextProtocol.Client; + +// Load environment variables from .env file +DotNetEnv.Env.Load(); + +// +// 1. Create MCP Toolbox client (SSE/HTTP) +// +var mcpToolboxClient = await McpClientFactory.CreateAsync( + new SseClientTransport(new SseClientTransportOptions() + { + Endpoint = new Uri("http://192.168.1.139/mcp"), + TransportMode = HttpTransportMode.StreamableHttp, + }, new HttpClient())); +// -- + +var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion( + DotNetEnv.Env.GetString("AZUREAI_DEPLOYMENT_NAME"), + DotNetEnv.Env.GetString("AZUREAI_DEPLOYMENT_ENDPOINT"), + DotNetEnv.Env.GetString("AZUREAI_DEPLOYMENT_API_KEY") + ) + .Build(); + +// +// 2. Register MCP Toolbox client as a tool +// +var tools = await mcpToolboxClient.ListToolsAsync().ConfigureAwait(false); + +// Print those tools +Console.WriteLine("// Available tools:"); +foreach (var t in tools) Console.WriteLine($"{t.Name}: {t.Description}"); +Console.WriteLine("// --"); + +// Load them as AI functions in the kernel +#pragma warning disable SKEXP0001 +kernel.Plugins.AddFromFunctions("MyComputerToolbox", tools.Select(aiFunction => aiFunction.AsKernelFunction())); +// -- + +var history = new ChatHistory(); +var chatCompletionService = kernel.GetRequiredService(); + +Console.Write("User > "); +string? userInput; + +while ((userInput = Console.ReadLine()) is not null) +{ + // Add user input + history.AddUserMessage(userInput); + + OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, + + }; + + // Get the response from the AI + var result = await chatCompletionService.GetChatMessageContentAsync( + history, + executionSettings: openAIPromptExecutionSettings, + kernel: kernel); + + // Print the results + Console.WriteLine("Assistant > " + result); + + // Add the message from the agent to the chat history + history.AddMessage(result.Role, result.Content ?? string.Empty); + + // Get user input again + Console.Write("User > "); +} diff --git a/tests/McpEndToEndTest/McpEndToEndTest.nfproj b/tests/McpEndToEndTest/McpEndToEndTest.nfproj index 982932e..1d87e77 100644 --- a/tests/McpEndToEndTest/McpEndToEndTest.nfproj +++ b/tests/McpEndToEndTest/McpEndToEndTest.nfproj @@ -21,6 +21,7 @@ + @@ -38,6 +39,9 @@ ..\..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + ..\..\packages\nanoFramework.System.Device.Wifi.1.5.133\lib\System.Device.Wifi.dll + ..\..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll @@ -55,8 +59,12 @@ + + + + diff --git a/tests/McpEndToEndTest/McpToolsClasses.cs b/tests/McpEndToEndTest/McpToolsClasses.cs index 3efcb39..ab037c5 100644 --- a/tests/McpEndToEndTest/McpToolsClasses.cs +++ b/tests/McpEndToEndTest/McpToolsClasses.cs @@ -2,15 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Reflection; +using nanoFramework.WebServer; using nanoFramework.WebServer.Mcp; namespace McpServerTests { public class Person { + [Description("A person object with basic details.")] public string Name { get; set; } public string Surname { get; set; } - public int Age { get; set; } = 30; // Default age + public string Age { get; set; } = "30"; // Default age public Address Address { get; set; } = new Address(); // Default address } @@ -24,10 +27,44 @@ public class Address public class McpTools { - [McpTool("process_person", "Processes a person object.", null, null, typeof(Person))] + [McpServerTool("echo","The echoed string")] + public static string Echo(string echo) => echo; + + [McpServerTool("process_person", "Processes a person object.", "the output is person processed.")] public static string ProcessPerson(Person person) { - return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; + //return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; + return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}"; //, Location: {person.Address.City}, {person.Address.Country}"; + } + + [McpServerTool("get_default_person", "Returns a default person object.", "the output is a default person object.")] + public Person GetDefaultPerson() + { + return new Person + { + Name = "John", + Surname = "Doe", + Age = "30", + Address = new Address + { + Street = "123 Main St", + City = "Anytown", + PostalCode = "12345", + Country = "USA" + } + }; + } + + [McpServerTool("get_default_address", "Returns a default address object.")] + public Address GetDefaultAddress() + { + return new Address + { + Street = "456 Elm St", + City = "Sample City", + PostalCode = "67890", + Country = "Sample Country" + }; } } } diff --git a/tests/McpEndToEndTest/Program.cs b/tests/McpEndToEndTest/Program.cs index 22783b9..8871e25 100644 --- a/tests/McpEndToEndTest/Program.cs +++ b/tests/McpEndToEndTest/Program.cs @@ -3,20 +3,57 @@ using System; using System.Diagnostics; +using System.Net; +using System.Net.NetworkInformation; using System.Threading; -//using nanoFramework.WebServer.Mcp; +using nanoFramework.Networking; +using nanoFramework.WebServer; +using nanoFramework.WebServer.Mcp; namespace McpEndToEndTest { - public class Program + public partial class Program { + private static WebServer _server; + public static void Main() { Debug.WriteLine("Hello from MCP Server!"); - //McpToolRegistry.DiscoverTools(new Type[] { typeof(McpServerTests.McpTools) }); + var res = WifiNetworkHelper.ConnectDhcp(Ssid, Password, requiresDateTime: true, token: new CancellationTokenSource(60_000).Token); + if (!res) + { + Debug.WriteLine("Impossible to connect to wifi, most likely invalid credentials"); + return; + } + + Debug.WriteLine($"Connected with wifi credentials. IP Address: {GetCurrentIPAddress()}"); + + McpToolRegistry.DiscoverTools(new Type[] { typeof(McpServerTests.McpTools) }); + + Debug.WriteLine("MCP Tools discovered and registered."); + + _server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }); + _server.CommandReceived += ServerCommandReceived; + // Start the server. + _server.Start(); Thread.Sleep(Timeout.Infinite); } + + private static void ServerCommandReceived(object obj, WebServerEventArgs e) + { + var url = e.Context.Request.RawUrl; + Debug.WriteLine($"{nameof(ServerCommandReceived)} {e.Context.Request.HttpMethod} {url}"); + WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound); + } + + public static string GetCurrentIPAddress() + { + NetworkInterface ni = NetworkInterface.GetAllNetworkInterfaces()[0]; + + // get first NI ( Wifi on ESP32 ) + return ni.IPv4Address.ToString(); + } } } diff --git a/tests/McpEndToEndTest/packages.config b/tests/McpEndToEndTest/packages.config index 958c4f3..dc88051 100644 --- a/tests/McpEndToEndTest/packages.config +++ b/tests/McpEndToEndTest/packages.config @@ -4,6 +4,7 @@ + diff --git a/tests/McpEndToEndTest/packages.lock.json b/tests/McpEndToEndTest/packages.lock.json index ddd01f9..23e1f39 100644 --- a/tests/McpEndToEndTest/packages.lock.json +++ b/tests/McpEndToEndTest/packages.lock.json @@ -26,6 +26,12 @@ "resolved": "1.5.67", "contentHash": "MjSipUB70vrxjqTm1KfKTUqqjd0wbweiNyYFXONi0XClrH6HXsuX2lhDqXM8NWuYnWyYOqx8y20sXbvsH+4brg==" }, + "nanoFramework.System.Device.Wifi": { + "type": "Direct", + "requested": "[1.5.133, 1.5.133]", + "resolved": "1.5.133", + "contentHash": "0AyJ6I7C+UWz8A2c+ChfYl/tdAroVZDxl7cVstQ9kbN0Ts8MEwD368Uoe8+pOpcJmjamTmg5iUDD9SMrW1nCuw==" + }, "nanoFramework.System.IO.Streams": { "type": "Direct", "requested": "[1.1.96, 1.1.96]", diff --git a/tests/McpEndToEndTest/requests.http b/tests/McpEndToEndTest/requests.http new file mode 100644 index 0000000..5562028 --- /dev/null +++ b/tests/McpEndToEndTest/requests.http @@ -0,0 +1,38 @@ +# This file is a collection of requests that can be executed with the REST Client extension for Visual Studio Code +# https://marketplace.visualstudio.com/items?itemName=humao.rest-client +# adjust your host here +@host=192.168.1.139:80 + +### + +GET http://{{host}}/tools HTTP/1.1 +Content-Type: application/json + +### + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"tools/list","params":{},"id":2,"jsonrpc":"2.0"} + +### + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"notifications/initialized","jsonrpc":"2.0"} + +### + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"McpClientTest","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"} + +### + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"tools/call","params":{"name":"process_person","arguments":{"Name":"John","Surname":"Doe","Address":"{\u0022Street\u0022:\u0022456 Elm St\u0022,\u0022City\u0022:\u0022Sample City\u0022,\u0022PostalCode\u0022:\u002267890\u0022,\u0022Country\u0022:\u0022Sample Country\u0022}"}},"id":12,"jsonrpc":"2.0"} + diff --git a/tests/McpServerTests/McpToolsClasses.cs b/tests/McpServerTests/McpToolsClasses.cs index 3efcb39..1a63b05 100644 --- a/tests/McpServerTests/McpToolsClasses.cs +++ b/tests/McpServerTests/McpToolsClasses.cs @@ -24,7 +24,7 @@ public class Address public class McpTools { - [McpTool("process_person", "Processes a person object.", null, null, typeof(Person))] + [McpServerTool("process_person", "Processes a person object.")] public static string ProcessPerson(Person person) { return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; From 705b4eba1729a6e853cd796c12eef8f5dc3897ab Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 26 Jun 2025 22:23:10 +0200 Subject: [PATCH 3/8] adding unit tests, more elements, fixing some issues, optimizations and docs! --- README.md | 347 ++++++++++++- .../McpServerController.cs | 108 ++-- .../McpToolRegistry.cs | 230 +++++---- .../McpToolsJsonHelper.cs | 40 ++ nanoFramework.WebServer.Mcp/ToolMetadata.cs | 5 + tests/McpClientTest/McpClientTest.cs | 2 +- tests/McpEndToEndTest/McpToolsClasses.cs | 8 +- tests/McpEndToEndTest/requests.http | 38 +- tests/McpServerTests/McpServerTests.nfproj | 26 +- tests/McpServerTests/McpToolRegistryTests.cs | 485 ++++++++++++++++++ .../McpServerTests/McpToolsAttributeTests.cs | 372 +++++++++++++- tests/McpServerTests/McpToolsClasses.cs | 33 -- tests/McpServerTests/packages.config | 7 + tests/McpServerTests/packages.lock.json | 42 ++ 14 files changed, 1542 insertions(+), 201 deletions(-) create mode 100644 tests/McpServerTests/McpToolRegistryTests.cs delete mode 100644 tests/McpServerTests/McpToolsClasses.cs diff --git a/README.md b/README.md index c963480..70f0115 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ----- -### Welcome to the .NET **nanoFramework** WebServer repository +### Welcome to the .NET **nanoFramework** WebServer repository including Model Context Protocol (MCP) ## Build status @@ -12,8 +12,9 @@ |:-|---|---| | nanoFramework.WebServer | [![Build Status](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_apis/build/status/nanoFramework.WebServer?repoName=nanoframework%2FnanoFramework.WebServer&branchName=main)](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65&repoName=nanoframework%2FnanoFramework.WebServer&branchName=main) | [![NuGet](https://img.shields.io/nuget/v/nanoFramework.WebServer.svg?label=NuGet&style=flat&logo=nuget)](https://www.nuget.org/packages/nanoFramework.WebServer/) | | nanoFramework.WebServer.FileSystem | [![Build Status](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_apis/build/status/nanoFramework.WebServer?repoName=nanoframework%2FnanoFramework.WebServer&branchName=main)](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65&repoName=nanoframework%2FnanoFramework.WebServer&branchName=main) | [![NuGet](https://img.shields.io/nuget/v/nanoFramework.WebServer.FileSystem.svg?label=NuGet&style=flat&logo=nuget)](https://www.nuget.org/packages/nanoFramework.WebServer.FileSystem/) | +| nanoFramework.WebServer.Mcp | [![Build Status](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_apis/build/status/nanoFramework.WebServer?repoName=nanoframework%2FnanoFramework.WebServer&branchName=main)](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65&repoName=nanoframework%2FnanoFramework.WebServer&branchName=main) | [![NuGet](https://img.shields.io/nuget/v/nanoFramework.WebServer.Mcp.svg?label=NuGet&style=flat&logo=nuget)](https://www.nuget.org/packages/nanoFramework.WebServer.Mcp/) | -## .NET nanoFramework WebServer +## .NET nanoFramework WebServer and Model Context Protocol (MCP) extension This library was coded by [Laurent Ellerbach](https://github.com/Ellerbach) who generously offered it to the .NET **nanoFramework** project. @@ -30,6 +31,7 @@ This is a simple nanoFramework WebServer. Features: - Helpers to return error code directly facilitating REST API - HTTPS support - [URL decode/encode](https://github.com/nanoframework/lib-nanoFramework.System.Net.Http/blob/develop/nanoFramework.System.Net.Http/Http/System.Net.HttpUtility.cs) +- **Model Context Protocol (MCP) support** for AI agent integration with automatic tool discovery and invocation. [MCP](https://github.com/modelcontextprotocol/) is a protocol specifically designed for a smooth integration with generative AI agents. The protocol is based over HTTP and this implementation is a minimal one allowing you an easy and working implementation with any kind of agent supporting MCP! More details in [this implementation here](#model-context-protocol-mcp-support). Limitations: @@ -481,6 +483,347 @@ There is a collection of postman tests `nanoFramework WebServer E2E Tests.postma The WebServerE2ETests project requires the name and credentials for the WiFi access point. That is stored in the WiFi.cs file that is not part of the git repository. Build the WebServerE2ETests to create a template for that file, then change the SSID and credentials. Your credentials will not be part of a commit. + +## Model Context Protocol (MCP) Support + +The nanoFramework WebServer provides comprehensive support for the Model Context Protocol (MCP), enabling AI agents and language models to directly interact with your embedded devices. MCP allows AI systems to discover, invoke, and receive responses from tools running on your nanoFramework device. + +### Overview + +The MCP implementation in nanoFramework WebServer includes: + +- **Automatic tool discovery** through reflection and attributes +- **JSON-RPC 2.0 compliant** request/response handling +- **Type-safe parameter handling** with automatic deserialization from JSON to .NET objects +- **Flexible authentication** options (none, basic auth, API key) +- **Complex object support** for both input parameters and return values +- **Robust error handling** and validation + +The supported version is [2025-03-26](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json). Only Server features are implemented. And there is no notification neither Server Sent Events (SSE) support. The returned type is only string. + +### Defining MCP Tools + +MCP tools are defined using the `[McpServerTool]` attribute on static or instance methods. The attribute accepts a tool name, description, and optional output description: + +```csharp +public class McpTools +{ + // Simple tool with primitive parameter and return type + [McpServerTool("echo", "Echoes the input string back to the caller")] + public static string Echo(string input) => input; + + // Tool with numeric parameters + [McpServerTool("calculate_square", "Calculates the square of a number minus 1")] + public static float CalculateSquare(float number) => number * number - 1; + + // Tool with complex object parameter + [McpServerTool("process_person", "Processes a person object and returns a summary", "A formatted string with person details")] + public static string ProcessPerson(Person person) + { + return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; + } + + // Tool returning complex objects + [McpServerTool("get_default_person", "Returns a default person object", "A person object with default values")] + public Person GetDefaultPerson() + { + return new Person + { + Name = "John", + Surname = "Doe", + Age = "30", + Address = new Address + { + Street = "123 Main St", + City = "Anytown", + PostalCode = "12345", + Country = "USA" + } + }; + } +} +``` + +> [!Important] +> Only none or 1 parameter is supported for the tools. .NET nanoFramework in the relection does not support names in functions, only types are available. And the AI Agent won't necessarily send in order the paramters. It means, it's not technically possible to know which parameter is which. If you need more than one parameter, create a class. Complex types as shown in the examples are supported. + +### Complex Object Definitions + +You can use complex objects as parameters and return types. Use the `[Description]` attribute to provide schema documentation: + +```csharp +public class Person +{ + [Description("The person's first name")] + public string Name { get; set; } + + public string Surname { get; set; } + + [Description("The person's age in years")] + public int Age { get; set; } = 30; + + public Address Address { get; set; } = new Address(); +} + +public class Address +{ + public string Street { get; set; } = "Unknown"; + public string City { get; set; } = "Unknown"; + public string PostalCode { get; set; } = "00000"; + public string Country { get; set; } = "Unknown"; +} +``` + +### Setting Up MCP Server + +To enable MCP support in your WebServer, follow these steps: + +```csharp +public static void Main() +{ + // Connect to WiFi (device-specific code) + var connected = WifiNetworkHelper.ConnectDhcp(Ssid, Password, requiresDateTime: true, token: new CancellationTokenSource(60_000).Token); + if (!connected) + { + Debug.WriteLine("Failed to connect to WiFi"); + return; + } + + // Step 1: Discover and register MCP tools + McpToolRegistry.DiscoverTools(new Type[] { typeof(McpTools) }); + Debug.WriteLine("MCP Tools discovered and registered."); + + // Step 2: Start WebServer with MCP controller + // You can add more types if you also want to use it as a Web Server + // Note: HTTPS and certs are also supported, see the pervious sections + using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) })) + { // Optional: Customize MCP server information and instructions + // This will override the default server name "nanoFramework" and version "1.0.0" + McpServerController.ServerName = "MyIoTDevice"; + McpServerController.ServerVersion = "2.1.0"; + + // Optional: Customize instructions sent to AI agents + // This will override the default instruction about single request limitation + McpServerController.Instructions = "This is my custom IoT device. Please send requests one at a time and wait for responses. Supports GPIO control and sensor readings."; + + server.Start(); + Thread.Sleep(Timeout.Infinite); + } +} +``` + +### MCP Authentication Options + +The MCP implementation supports three authentication modes: + +#### 1. No Authentication (Default) +```csharp +// Use McpServerController for no authentication +var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }); +``` + +#### 2. Basic Authentication +```csharp +// Use McpServerBasicAuthenticationController for basic auth +var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerBasicAuthenticationController) }); +server.Credential = new NetworkCredential("username", "password"); +``` + +#### 3. API Key Authentication +```csharp +// Use McpServerKeyAuthenticationController for API key auth +var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerKeyAuthenticationController) }); +server.ApiKey = "your-secret-api-key"; +``` + +### MCP Request/Response Examples + +You have a collection of [tests queries](./tests/McpEndToEndTest/requests.http) available. To run them, install the [VS Code REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). And I encourage you to get familiar with the way of working using the [McpEndToEndTest](./tests/McpEndToEndTest/) project. + +#### Tool Discovery Request + +```json +POST /mcp +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list" +} +``` + +#### Tool Discovery Response + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "tools": [ + { + "name": "echo", + "description": "Echoes the input string back to the caller", + "inputSchema": { + "type": "object", + "properties": { + "value": {"type": "string"} + }, + } + }, + { + "name": "process_person", + "description": "Processes a person object and returns a summary", + "inputSchema": { + "type": "object", + "properties": { + "person": { + "type": "object", + "properties": { + "Name": {"type": "string", "description": "The person's first name"}, + "Surname": {"type": "string"}, + "Age": {"type": "number", "description": "The person's age in years"}, + "Address": { + "type": "object", + "properties": { + "Street": {"type": "string"}, + "City": {"type": "string"}, + "PostalCode": {"type": "string"}, + "Country": {"type": "string"} + } + } + } + } + }, + } + } + ] + } +} +``` + +> [!Note] +> the `required` field is not supported. You'll have to manage in the code the fact that you may not receive all the elements. +> In case, you require more elements, just send back to the agent that you need the missing fields, it will ask the user and send you back a proper query. With history, it will learn and call you properly the next time in most of the cases. + +#### Tool Invocation Request + +```json +POST /mcp +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "process_person", + "arguments": { + "person": { + "Name": "Alice", + "Surname": "Smith", + "Age": "28", + "Address": { + "Street": "789 Oak Ave", + "City": "Springfield", + "PostalCode": "54321", + "Country": "USA" + } + } + } + } +} +``` + +> [!Note] +> Most agents will not send you numbers as number in JSON serializaton, like in the example with the age. The library will always try to convert the serialized element as the target type. It can be sent as a string, if a number is inside, it will be deserialized properly. + +#### Tool Invocation Response + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "content": [ + { + "type": "text", + "text": "Processed: Alice Smith, Age: 28, Location: Springfield, USA" + } + ] + } +} +``` + +### MCP Protocol Flow + +1. **Initialization**: AI agent sends `initialize` request to establish connection +2. **Tool Discovery**: Agent requests available tools via `tools/list` +3. **Tool Invocation**: Agent calls specific tools via `tools/call` with parameters +4. **Response Handling**: Server returns results in MCP-compliant format + +### Error Handling + +The MCP implementation provides robust error handling: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32601, + "message": "Method not found" + } +} +``` + +Common error codes: +- `-32601`: Method not found +- `-32602`: Invalid parameters or internal error + +### Best Practices + +1. **Tool Design**: Keep tools focused on single responsibilities +2. **Type Safety**: Use strongly-typed parameters and return values +3. **Documentation**: Provide clear descriptions for tools and complex parameters +4. **Error Handling**: Implement proper validation in your tool methods +5. **Memory Management**: Be mindful of memory usage on embedded devices +6. **Authentication**: Use appropriate authentication for your security requirements +7. **SSL**: Use SSL encryption with certificate to protect the data transfer especially if you expose your service over Internet + +### Complete Example + +For a complete working example, see the [McpEndToEndTest](tests/McpEndToEndTest/) project which demonstrates: + +- Tool discovery and registration +- Various parameter types (primitive, complex objects) +- WiFi connectivity setup +- Server configuration with MCP support + +### .NET 10 MCP Client with Azure OpenAI + +The repository also includes a [.NET 10 MCP client example](tests/McpClientTest/) that demonstrates how to connect to your nanoFramework MCP server from a full .NET application using Azure OpenAI and Semantic Kernel. This client example shows: + +- **Azure OpenAI integration** using Semantic Kernel +- **MCP client connectivity** to nanoFramework devices +- **Automatic tool discovery** and registration as AI functions +- **Interactive chat interface** that can invoke tools on your embedded device +- **Real-time communication** between AI agents and nanoFramework hardware + +The client uses the official ModelContextProtocol NuGet package and can automatically discover and invoke any tools exposed by your nanoFramework MCP server, enabling seamless AI-to-hardware interactions. + +```csharp +// Example: Connect .NET client to nanoFramework MCP server +var mcpToolboxClient = await McpClientFactory.CreateAsync( + new SseClientTransport(new SseClientTransportOptions() + { + Endpoint = new Uri("http://192.168.1.139/mcp"), // Your nanoFramework device IP + TransportMode = HttpTransportMode.StreamableHttp, + }, new HttpClient())); + +// Register discovered tools with Semantic Kernel +var tools = await mcpToolboxClient.ListToolsAsync(); +kernel.Plugins.AddFromFunctions("MyDeviceTools", tools.Select(t => t.AsKernelFunction())); +``` + +This comprehensive MCP support enables your nanoFramework devices to seamlessly integrate with AI systems and language models, opening up new possibilities for intelligent embedded applications. + ## Feedback and documentation For documentation, providing feedback, issues and finding out how to contribute please refer to the [Home repo](https://github.com/nanoframework/Home). diff --git a/nanoFramework.WebServer.Mcp/McpServerController.cs b/nanoFramework.WebServer.Mcp/McpServerController.cs index 84ef172..ca8a929 100644 --- a/nanoFramework.WebServer.Mcp/McpServerController.cs +++ b/nanoFramework.WebServer.Mcp/McpServerController.cs @@ -15,6 +15,26 @@ namespace nanoFramework.WebServer.Mcp /// public class McpServerController { + /// + /// The supported version of the MCP protocol. + /// + public const string SupportedVersion = "2025-03-26"; + + /// + /// Gets or sets the server name. + /// + public static string ServerName { get; set; } = "nanoFramework"; + + /// + /// Gets or sets the server version. + /// + public static string ServerVersion { get; set; } = "1.0.0"; + + /// + /// Gets or sets the instructions for using the MCP server. + /// + public static string Instructions { get; set; } = "This is an embedded device and only 1 request at a time should be sent."; + /// /// Handles POST requests to the "mcp" route. /// Processes the incoming request, invokes the specified tool with provided parameters, and writes the result to the response stream in JSON format. @@ -39,13 +59,6 @@ public void HandleMcpRequest(WebServerEventArgs e) Hashtable request = (Hashtable)JsonConvert.DeserializeObject(requestBody, typeof(Hashtable)); -//#if DEBUG -// foreach (string key in request.Keys) -// { -// Debug.WriteLine($"Key: {key}, Value: {request[key]}"); -// } -//#endif - // Sets jsonrpc version sb.Append("{\"jsonrpc\": \"2.0\""); // Check if we have an id if yes, add it to the answer @@ -66,52 +79,54 @@ public void HandleMcpRequest(WebServerEventArgs e) if (request["method"].ToString() == "initialize") { - // TODO: check the received version and adjust with error message or set this version - sb.Append(",\"result\":{\"protocolVersion\":\"2025-03-26\""); + // Check if client sent params with protocolVersion + if (request.ContainsKey("params") && request["params"] is Hashtable initParams) + { + if (initParams.ContainsKey("protocolVersion")) + { + string clientVersion = initParams["protocolVersion"].ToString(); + if (clientVersion != SupportedVersion) + { + sb.Append($",\"error\":{{\"code\":-32602,\"message\":\"Unsupported protocol version\",\"data\":{{\"supported\":[\"{SupportedVersion}\"],\"requested\":\"{clientVersion}\"}}}}}}"); + WebServer.OutPutStream(e.Context.Response, sb.ToString()); + return; + } + } + } + + sb.Append($",\"result\":{{\"protocolVersion\":\"{SupportedVersion}\""); // Add capabilities sb.Append($",\"capabilities\":{{\"logging\":{{}},\"prompts\":{{\"listChanged\":false}},\"resources\":{{\"subscribe\":false,\"listChanged\":false}},\"tools\":{{\"listChanged\":false}}}}"); // Add serverInfo - sb.Append($",\"serverInfo\":{{\"name\":\"nanoFramework\",\"version\":\"1.0.0\"}}"); + sb.Append($",\"serverInfo\":{{\"name\":\"{ServerName}\",\"version\":\"{ServerVersion}\"}}"); // Add instructions - sb.Append($",\"instructions\":\"This is an embedded device and only 1 request at a time should be sent. Make sure you pass numbers as numbers and not as string.\"}}}}"); - //sb.Append("\r\n\r\n{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}"); - WebServer.OutPutStream(e.Context.Response, sb.ToString()); - return; + sb.Append($",\"instructions\":\"{Instructions}\"}}}}"); } - - if (request["method"].ToString() == "tools/list") + else if (request["method"].ToString() == "tools/list") { // This is a request for the list of tools string toolListJson = McpToolRegistry.GetToolMetadataJson(); sb.Append($",\"result\":{{{toolListJson}}}}}"); - WebServer.OutPutStream(e.Context.Response, sb.ToString()); - return; } - - // Check if the method starts with "tools/call" and extract the method name to call the tool - if (request["method"].ToString() == "tools/call") + else if (request["method"].ToString() == "tools/call") { - //string param = JsonConvert.SerializeObject(((Hashtable)request["params"])["arguments"]); string toolName = ((Hashtable)request["params"])["name"].ToString(); Hashtable param = ((Hashtable)request["params"])["arguments"] == null ? null : (Hashtable)((Hashtable)request["params"])["arguments"]; string result = McpToolRegistry.InvokeTool(toolName, param); - //Debug.WriteLine($"Tool: {toolName}, Param: {param}"); - - sb.Append($",\"result\":{{\"content\":[{{\"type\":\"text\",\"text\":{JsonConvert.SerializeObject(result)}}}]}}}}"); - WebServer.OutPutStream(e.Context.Response, sb.ToString()); - return; + sb.Append($",\"result\":{{\"content\":[{{\"type\":\"text\",\"text\":{result}}}]}}}}"); } else { sb.Append($",\"error\":{{\"code\":-32601,\"message\":\"Method not found\"}}}}"); - WebServer.OutPutStream(e.Context.Response, sb.ToString()); - return; } + + WebServer.OutPutStream(e.Context.Response, sb.ToString()); + return; } } catch (Exception ex) @@ -119,40 +134,5 @@ public void HandleMcpRequest(WebServerEventArgs e) WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"id\":{id},\"error\":{{\"code\":-32602,\"message\":\"{ex.Message}\"}}}}"); } } - - /// - /// Handles GET requests to the "tools" route. - /// Returns a JSON list of all available tools and their metadata. - /// - /// The web server event arguments containing the HTTP context and request/response information. - [Route("tools"), Method("GET")] - public void GetToolList(WebServerEventArgs e) - { - e.Context.Response.ContentType = "application/json"; - WebServer.OutputHttpCode(e.Context.Response, System.Net.HttpStatusCode.NotFound); - return; - - try - { - Debug.WriteLine($"GET request for tool list received: {e.Context.Request.RawUrl}"); - string toolListJson = McpToolRegistry.GetToolMetadataJson(); - WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"result\":{{{toolListJson}}}}}"); - } - catch (Exception ex) - { - WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"{ex.Message}\"}}}}"); - } - - // Free up the memory used - Runtime.Native.GC.Run(true); - } - - /// - /// Handles GET requests to the "mcp" route. - /// Returns the same tool list as . - /// - /// The web server event arguments containing the HTTP context and request/response information. - [Route("mcp"), Method("GET")] - public void GetToolMcpList(WebServerEventArgs e) => GetToolList(e); } } diff --git a/nanoFramework.WebServer.Mcp/McpToolRegistry.cs b/nanoFramework.WebServer.Mcp/McpToolRegistry.cs index af0949e..236918f 100644 --- a/nanoFramework.WebServer.Mcp/McpToolRegistry.cs +++ b/nanoFramework.WebServer.Mcp/McpToolRegistry.cs @@ -28,7 +28,8 @@ public static void DiscoverTools(Type[] mcpTools) { if (isInitialized) { - return; // Tools already discovered + // Tools already discovered + return; } foreach (Type mcpTool in mcpTools) @@ -51,10 +52,15 @@ public static void DiscoverTools(Type[] mcpTools) { var parameters = method.GetParameters(); string inputType = string.Empty; - if (parameters.Length > 0) + // We only support either no parameters or one parameter for now + if (parameters.Length == 1) { inputType = McpToolJsonHelper.GenerateInputJson(parameters[0].ParameterType); } + else if (parameters.Length > 1) + { + continue; + } tools.Add(attribute.Name, new ToolMetadata { @@ -62,7 +68,8 @@ public static void DiscoverTools(Type[] mcpTools) Description = attribute.Description, InputType = inputType, OutputType = McpToolJsonHelper.GenerateOutputJson(method.ReturnType, attribute.OutputDescription), - Method = method + Method = method, + MethodType = parameters.Length > 0 ? parameters[0].ParameterType : null, }); } } @@ -99,7 +106,6 @@ public static string GetToolMetadataJson() sb.Remove(sb.Length - 1, 1); sb.Append("],\"nextCursor\":null"); return sb.ToString(); - } catch (Exception) { @@ -107,22 +113,6 @@ public static string GetToolMetadataJson() } } - private static bool IsPrimitiveType(Type type) - { - return type == typeof(bool) || - type == typeof(byte) || - type == typeof(sbyte) || - type == typeof(char) || - type == typeof(double) || - type == typeof(float) || - type == typeof(int) || - type == typeof(uint) || - type == typeof(long) || - type == typeof(ulong) || - type == typeof(short) || - type == typeof(ushort); - } - private static object CreateInstance(Type type) { // Get the default constructor @@ -135,6 +125,97 @@ private static object CreateInstance(Type type) return constructor.Invoke(new object[0]); } + /// + /// Converts a value to the specified primitive type with appropriate type conversion and error handling. + /// + /// The value to convert. + /// The target primitive type to convert to. + /// The converted value as the target type. + private static object ConvertToPrimitiveType(object value, Type targetType) + { + if (value == null) + { + return null; + } + + if (targetType == typeof(string)) + { + return value.ToString(); + } + else if (targetType == typeof(int)) + { + return Convert.ToInt32(value.ToString()); + } + else if (targetType == typeof(double)) + { + return Convert.ToDouble(value.ToString()); + } + else if (targetType == typeof(bool)) + { + // If it's a 0 or a 1 + if (value.ToString().Length == 1) + { + try + { + return Convert.ToBoolean(Convert.ToByte(value.ToString())); + } + catch (Exception) + { + // Nothing on purpose, we will handle it below + } + } + + // Then it's a tex + return value.ToString().ToLower() == "true"; + } + else if (targetType == typeof(long)) + { + return Convert.ToInt64(value.ToString()); + } + else if (targetType == typeof(float)) + { + return Convert.ToSingle(value.ToString()); + } + else if (targetType == typeof(byte)) + { + return Convert.ToByte(value.ToString()); + } + else if (targetType == typeof(short)) + { + return Convert.ToInt16(value.ToString()); + } + else if (targetType == typeof(char)) + { + try + { + return Convert.ToChar(Convert.ToUInt16(value.ToString())); + } + catch (Exception) + { + return string.IsNullOrEmpty(value.ToString()) ? '\0' : value.ToString()[0]; + } + } + else if (targetType == typeof(uint)) + { + return Convert.ToUInt32(value.ToString()); + } + else if (targetType == typeof(ulong)) + { + return Convert.ToUInt64(value.ToString()); + } + else if (targetType == typeof(ushort)) + { + return Convert.ToUInt16(value.ToString()); + } + else if (targetType == typeof(sbyte)) + { + return Convert.ToSByte(value.ToString()); + } + + // Fallback - return the original value + return value; + } + /// /// Recursively deserializes a Hashtable into a strongly-typed object by mapping properties and handling nested objects. /// @@ -149,7 +230,7 @@ private static object DeserializeFromHashtable(Hashtable hashtable, Type targetT } // For primitive types and strings, try direct conversion - if (IsPrimitiveType(targetType) || targetType == typeof(string)) + if (McpToolJsonHelper.IsPrimitiveType(targetType) || targetType == typeof(string)) { // This shouldn't happen in our context, but handle it gracefully return hashtable; @@ -187,62 +268,12 @@ private static object DeserializeFromHashtable(Hashtable hashtable, Type targetT try { // Get the parameter type of the setter method (which is the property type) - Type propertyType = method.GetParameters()[0].ParameterType; - - // Handle primitive types and strings - if (IsPrimitiveType(propertyType) || propertyType == typeof(string)) + Type propertyType = method.GetParameters()[0].ParameterType; // Handle primitive types and strings + if (McpToolJsonHelper.IsPrimitiveType(propertyType) || propertyType == typeof(string)) { - // Direct assignment for primitive types and strings - if (propertyType == typeof(string)) - { - method.Invoke(instance, new object[] { value.ToString() }); - } - else if (propertyType == typeof(int)) - { - method.Invoke(instance, new object[] { Convert.ToInt32(value.ToString()) }); - } - else if (propertyType == typeof(double)) - { - method.Invoke(instance, new object[] { Convert.ToDouble(value.ToString()) }); - } - else if (propertyType == typeof(bool)) - { - try - { - method.Invoke(instance, new object[] { Convert.ToBoolean(Convert.ToByte(value.ToString())) }); - } - catch (Exception) - { - method.Invoke(instance, new object[] { value.ToString().ToLower() == "true" ? true : false }); - } - } - else if (propertyType == typeof(long)) - { - method.Invoke(instance, new object[] { Convert.ToInt64(value.ToString()) }); - } - else if (propertyType == typeof(float)) - { - method.Invoke(instance, new object[] { Convert.ToSingle(value.ToString()) }); - } - else if (propertyType == typeof(byte)) - { - method.Invoke(instance, new object[] { Convert.ToByte(value.ToString()) }); - } - else if (propertyType == typeof(short)) - { - method.Invoke(instance, new object[] { Convert.ToInt16(value.ToString()) }); - } - else if (propertyType == typeof(char)) - { - try - { - method.Invoke(instance, new object[] { Convert.ToChar(Convert.ToUInt16(value.ToString())) }); - } - catch (Exception) - { - method.Invoke(instance, new object[] { string.IsNullOrEmpty(value.ToString()) ? '\0' : value.ToString()[0] }); - } - } + // Use the centralized conversion function + object convertedValue = ConvertToPrimitiveType(value, propertyType); + method.Invoke(instance, new object[] { convertedValue }); } else { @@ -286,24 +317,20 @@ public static string InvokeTool(string toolName, Hashtable parameter) ToolMetadata toolMetadata = (ToolMetadata)tools[toolName]; MethodInfo method = toolMetadata.Method; Debug.WriteLine($"Tool name: {toolName}, method: {method.Name}"); - ParameterInfo[] parametersInfo = method.GetParameters(); -#if DEBUG - foreach(ParameterInfo parameterInfo in parametersInfo) - { - Debug.WriteLine($"method type: {parameterInfo.ParameterType.FullName}"); - } -#endif object[] methodParams = null; - if (parametersInfo.Length > 0) + if (toolMetadata.MethodType != null) { - methodParams = new object[parametersInfo.Length]; - Type paramType = parametersInfo[0].ParameterType; - - if (IsPrimitiveType(paramType) || paramType == typeof(string)) + methodParams = new object[1]; + Type paramType = toolMetadata.MethodType; + if (McpToolJsonHelper.IsPrimitiveType(paramType) || paramType == typeof(string)) { - // For primitive types, use direct assignment - methodParams[0] = parameter; + // For primitive types, extract the "value" key and convert to target type + object value = parameter["value"]; + if (value != null) + { + methodParams[0] = ConvertToPrimitiveType(value, paramType); + } } else { @@ -313,7 +340,28 @@ public static string InvokeTool(string toolName, Hashtable parameter) } object result = method.Invoke(null, methodParams); - return JsonConvert.SerializeObject(result); + + // Handle serialization based on return type + if (result == null) + { + return "null"; + } + + Type resultType = result.GetType(); + + // For strings, return as-is with quotes + // For primitive types, convert to string and add quotes + if (McpToolJsonHelper.IsPrimitiveType(resultType) || resultType == typeof(string)) + { + var stringResult = result.GetType() == typeof(bool) ? result.ToString().ToLower() : result.ToString(); + return "\"" + stringResult + "\""; + } + // For complex objects, serialize to JSON and add quotes around the entire JSON + else + { + string jsonResult = JsonConvert.SerializeObject(result); + return JsonConvert.SerializeObject(jsonResult); + } } throw new Exception("Tool not found"); diff --git a/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs b/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs index bcf15e3..c06cc29 100644 --- a/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs +++ b/nanoFramework.WebServer.Mcp/McpToolsJsonHelper.cs @@ -23,12 +23,35 @@ public static class McpToolJsonHelper public static string GenerateInputJson(Type inputType) { StringBuilder sb = new StringBuilder(); + sb.Append("{\"type\":\"object\",\"properties\":{"); AppendInputPropertiesJson(sb, inputType, true); sb.Append("},\"required\":[]}"); + return sb.ToString(); } + /// + /// Checks if the specified is a primitive type. + /// + /// The to check. + /// true if the type is a primitive type; otherwise, false. + public static bool IsPrimitiveType(Type type) + { + return type == typeof(bool) || + type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(char) || + type == typeof(double) || + type == typeof(float) || + type == typeof(int) || + type == typeof(uint) || + type == typeof(long) || + type == typeof(ulong) || + type == typeof(short) || + type == typeof(ushort); + } + /// /// Generates a JSON object describing the output schema for a tool, including type, description, and nested properties if applicable. /// @@ -118,6 +141,23 @@ private static string GetTypeDescription(MethodInfo method, string propName) private static void AppendInputPropertiesJson(StringBuilder sb, Type type, bool isFirst) { + // If it's a primitive type or string, create a single property entry + if (IsPrimitiveType(type) || type == typeof(string)) + { + string mappedType = MapType(type); + if (!isFirst) + { + sb.Append(","); + } + + sb.Append("\"value\":{"); + sb.Append("\"type\":\"").Append(mappedType).Append("\","); + sb.Append("\"description\":\"Input parameter of type ").Append(type.Name).Append("\""); + sb.Append("}"); + return; + } + + // For complex types, analyze methods to find properties MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); for (int i = 0; i < methods.Length; i++) diff --git a/nanoFramework.WebServer.Mcp/ToolMetadata.cs b/nanoFramework.WebServer.Mcp/ToolMetadata.cs index 9a4f6e8..5e07ffc 100644 --- a/nanoFramework.WebServer.Mcp/ToolMetadata.cs +++ b/nanoFramework.WebServer.Mcp/ToolMetadata.cs @@ -41,6 +41,11 @@ public class ToolMetadata /// public MethodInfo Method { get; set; } + /// + /// Gets or sets the type of the method associated with the tool. + /// + public Type MethodType { get; set; } + /// /// Returns a JSON string representation of the tool metadata. /// diff --git a/tests/McpClientTest/McpClientTest.cs b/tests/McpClientTest/McpClientTest.cs index aeb61d0..5767668 100644 --- a/tests/McpClientTest/McpClientTest.cs +++ b/tests/McpClientTest/McpClientTest.cs @@ -45,7 +45,7 @@ // Load them as AI functions in the kernel #pragma warning disable SKEXP0001 -kernel.Plugins.AddFromFunctions("MyComputerToolbox", tools.Select(aiFunction => aiFunction.AsKernelFunction())); +kernel.Plugins.AddFromFunctions("nanoFramework", tools.Select(aiFunction => aiFunction.AsKernelFunction())); // -- var history = new ChatHistory(); diff --git a/tests/McpEndToEndTest/McpToolsClasses.cs b/tests/McpEndToEndTest/McpToolsClasses.cs index ab037c5..4bae31e 100644 --- a/tests/McpEndToEndTest/McpToolsClasses.cs +++ b/tests/McpEndToEndTest/McpToolsClasses.cs @@ -27,14 +27,16 @@ public class Address public class McpTools { - [McpServerTool("echo","The echoed string")] + [McpServerTool("echo", "The echoed string")] public static string Echo(string echo) => echo; + [McpServerTool("super_math", "make a complex super cool math")] + public static float SuperMAth(float a) => a * a - 1; + [McpServerTool("process_person", "Processes a person object.", "the output is person processed.")] public static string ProcessPerson(Person person) { - //return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; - return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}"; //, Location: {person.Address.City}, {person.Address.Country}"; + return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; } [McpServerTool("get_default_person", "Returns a default person object.", "the output is a default person object.")] diff --git a/tests/McpEndToEndTest/requests.http b/tests/McpEndToEndTest/requests.http index 5562028..4e27a9b 100644 --- a/tests/McpEndToEndTest/requests.http +++ b/tests/McpEndToEndTest/requests.http @@ -3,19 +3,34 @@ # adjust your host here @host=192.168.1.139:80 -### +### this will get a 404 error GET http://{{host}}/tools HTTP/1.1 Content-Type: application/json -### +### This will get the list of the tools available in the MCP server POST http://{{host}}/mcp HTTP/1.1 Content-Type: application/json {"method":"tools/list","params":{},"id":2,"jsonrpc":"2.0"} -### +### This is the initialization request to the MCP server. + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"McpClientTest","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"} + +### This is the initialization request to the MCP server but with a wrong version not matching. Should return an error + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"McpClientTest","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"} + + +### This is send to initialize the MCP server and should return a 200 OK response POST http://{{host}}/mcp HTTP/1.1 Content-Type: application/json @@ -27,12 +42,25 @@ Content-Type: application/json POST http://{{host}}/mcp HTTP/1.1 Content-Type: application/json -{"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"McpClientTest","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"} +{"method":"tools/call","params":{"name":"process_person","arguments":{"Name":"John","Surname":"Doe","Address":"{\u0022Street\u0022:\u0022456 Elm St\u0022,\u0022City\u0022:\u0022Sample City\u0022,\u0022PostalCode\u0022:\u002267890\u0022,\u0022Country\u0022:\u0022Sample Country\u0022}"}},"id":12,"jsonrpc":"2.0"} ### POST http://{{host}}/mcp HTTP/1.1 Content-Type: application/json -{"method":"tools/call","params":{"name":"process_person","arguments":{"Name":"John","Surname":"Doe","Address":"{\u0022Street\u0022:\u0022456 Elm St\u0022,\u0022City\u0022:\u0022Sample City\u0022,\u0022PostalCode\u0022:\u002267890\u0022,\u0022Country\u0022:\u0022Sample Country\u0022}"}},"id":12,"jsonrpc":"2.0"} +{"method":"tools/call","params":{"name":"echo","arguments":{"value":"Laurent is the best"}},"id":8,"jsonrpc":"2.0"} + +### + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json + +{"method":"tools/call","params":{"name":"super_math","arguments":{"value":"3.14"}},"id":3,"jsonrpc":"2.0"} + +### + +POST http://{{host}}/mcp HTTP/1.1 +Content-Type: application/json +{"method":"tools/call","params":{"name":"get_default_address","arguments":{}},"id":5,"jsonrpc":"2.0"} diff --git a/tests/McpServerTests/McpServerTests.nfproj b/tests/McpServerTests/McpServerTests.nfproj index 268c631..6e54e28 100644 --- a/tests/McpServerTests/McpServerTests.nfproj +++ b/tests/McpServerTests/McpServerTests.nfproj @@ -27,20 +27,41 @@ $(MSBuildProjectDirectory)\nano.runsettings + - ..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + ..\..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll + + + ..\..\packages\nanoFramework.System.Collections.1.5.67\lib\nanoFramework.System.Collections.dll + + + ..\..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + ..\..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.TestFramework.dll ..\..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.UnitTestLauncher.exe + + ..\..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\..\packages\nanoFramework.System.Net.1.11.43\lib\System.Net.dll + + + ..\..\packages\nanoFramework.System.Net.Http.Server.1.5.196\lib\System.Net.Http.dll + + + ..\..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + @@ -50,5 +71,8 @@ + + + \ No newline at end of file diff --git a/tests/McpServerTests/McpToolRegistryTests.cs b/tests/McpServerTests/McpToolRegistryTests.cs new file mode 100644 index 0000000..67341aa --- /dev/null +++ b/tests/McpServerTests/McpToolRegistryTests.cs @@ -0,0 +1,485 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using nanoFramework.TestFramework; +using nanoFramework.WebServer.Mcp; + +namespace McpServerTests +{ + // Test parameter classes + public class TestParameter + { + public string Name { get; set; } + public int Value { get; set; } + } + + public class NestedTestParameter + { + public string Title { get; set; } + public TestParameter Details { get; set; } + public bool IsEnabled { get; set; } + } + + // Test tool classes with MCP tools + public static class TestToolsClass + { + [McpServerTool("simple_tool", "A simple test tool")] + public static string SimpleTool(string input) + { + return $"Processed: {input}"; + } + + [McpServerTool("complex_tool", "A complex test tool", "Complex result")] + public static TestParameter ComplexTool(TestParameter param) + { + return new TestParameter { Name = param.Name + "_processed", Value = param.Value * 2 }; + } + + [McpServerTool("nested_tool", "A nested parameter tool", "Nested result")] + public static string NestedTool(NestedTestParameter param) + { + return $"{param.Title}: {param.Details.Name} = {param.Details.Value}, Enabled: {param.IsEnabled}"; + } + + [McpServerTool("primitive_int_tool", "A primitive int tool")] + public static int PrimitiveIntTool(int number) + { + return number * 2; + } + + [McpServerTool("primitive_bool_tool", "A primitive bool tool")] + public static bool PrimitiveBoolTool(bool flag) + { + return !flag; + } + + [McpServerTool("no_param_tool", "A tool with no parameters")] + public static string NoParamTool() + { + return "No parameters required"; + } + + // Method without attribute - should be ignored + public static string NonToolMethod(string input) + { + return input; + } + } + + public static class EmptyToolsClass + { + // No MCP tools + public static string RegularMethod() + { + return "Not a tool"; + } + } + [TestClass] + public class McpToolRegistryTests + { + [TestMethod] + public void TestDiscoverToolsAndGetMetadataSimple() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + Assert.IsTrue(metadataJson.Contains("\"tools\":["), "Metadata should contain tools array"); + Assert.IsTrue(metadataJson.Contains("\"nextCursor\":null"), "Metadata should contain nextCursor"); + Assert.IsTrue(metadataJson.Contains("simple_tool"), "Metadata should contain simple_tool"); + Assert.IsTrue(metadataJson.Contains("complex_tool"), "Metadata should contain complex_tool"); + Assert.IsTrue(metadataJson.Contains("nested_tool"), "Metadata should contain nested_tool"); + Assert.IsTrue(metadataJson.Contains("primitive_int_tool"), "Metadata should contain primitive_int_tool"); + Assert.IsTrue(metadataJson.Contains("primitive_bool_tool"), "Metadata should contain primitive_bool_tool"); + Assert.IsTrue(metadataJson.Contains("no_param_tool"), "Metadata should contain no_param_tool"); + Assert.IsFalse(metadataJson.Contains("NonToolMethod"), "Metadata should not contain non-tool methods"); + } + + [TestMethod] + public void TestDiscoverToolsEmptyClass() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(EmptyToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + Assert.IsTrue(metadataJson.Contains("\"tools\":["), "Metadata should contain tools array"); + Assert.IsTrue(metadataJson.Contains("\"nextCursor\":null"), "Metadata should contain nextCursor"); + // Should have empty tools array or just the tools from previous test + } + + [TestMethod] + public void TestDiscoverToolsMultipleClasses() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass), typeof(EmptyToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + Assert.IsTrue(metadataJson.Contains("simple_tool"), "Metadata should contain tools from TestToolsClass"); + Assert.IsFalse(metadataJson.Contains("RegularMethod"), "Metadata should not contain regular methods"); + } + + [TestMethod] + public void TestGetMetadataJsonStructure() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + + // Check overall structure + Assert.IsTrue(metadataJson.StartsWith("\"tools\":["), "Metadata should start with tools array"); + Assert.IsTrue(metadataJson.EndsWith("],\"nextCursor\":null"), "Metadata should end with nextCursor"); + + // Check for tool properties + Assert.IsTrue(metadataJson.Contains("\"name\":"), "Metadata should contain tool names"); + Assert.IsTrue(metadataJson.Contains("\"description\":"), "Metadata should contain tool descriptions"); + Assert.IsTrue(metadataJson.Contains("\"inputSchema\":"), "Metadata should contain input schemas"); + + // Check specific tool content + Assert.IsTrue(metadataJson.Contains("A simple test tool"), "Metadata should contain tool descriptions"); + Assert.IsTrue(metadataJson.Contains("A complex test tool"), "Metadata should contain complex tool description"); + } + + [TestMethod] + public void TestGetMetadataJsonWithPrimitiveTypes() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + + // Check for primitive type handling + Assert.IsTrue(metadataJson.Contains("primitive_int_tool"), "Metadata should contain primitive int tool"); + Assert.IsTrue(metadataJson.Contains("primitive_bool_tool"), "Metadata should contain primitive bool tool"); + + // Verify primitive types are handled correctly in input schema + Assert.IsTrue(metadataJson.Contains("\"type\":\"object\""), "Primitive inputs should be wrapped in object schema"); + Assert.IsTrue(metadataJson.Contains("\"value\""), "Primitive inputs should have value property"); + } + + [TestMethod] + public void TestGetMetadataJsonWithComplexTypes() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + + // Check for complex type handling + Assert.IsTrue(metadataJson.Contains("complex_tool"), "Metadata should contain complex tool"); + Assert.IsTrue(metadataJson.Contains("nested_tool"), "Metadata should contain nested tool"); + + // Verify complex types have proper object schemas + Assert.IsTrue(metadataJson.Contains("\"properties\""), "Complex types should have properties"); + Assert.IsTrue(metadataJson.Contains("Name"), "Complex types should include property names"); + Assert.IsTrue(metadataJson.Contains("Value"), "Complex types should include property names"); + } + + [TestMethod] + public void TestGetMetadataJsonWithNoParameterTool() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + + // Act + McpToolRegistry.DiscoverTools(toolTypes); + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null"); + Assert.IsTrue(metadataJson.Contains("no_param_tool"), "Metadata should contain no parameter tool"); + + // Tools with no parameters should still have valid structure + Assert.IsTrue(metadataJson.Contains("A tool with no parameters"), "Should contain no-param tool description"); + } + + [TestMethod] + public void TestDiscoverToolsCalledMultipleTimes() + { + // Arrange + Type[] toolTypes1 = new Type[] { typeof(TestToolsClass) }; + Type[] toolTypes2 = new Type[] { typeof(EmptyToolsClass) }; + + // Act - Call DiscoverTools multiple times + McpToolRegistry.DiscoverTools(toolTypes1); + string firstCall = McpToolRegistry.GetToolMetadataJson(); + + McpToolRegistry.DiscoverTools(toolTypes2); // Should be ignored as already initialized + string secondCall = McpToolRegistry.GetToolMetadataJson(); + + // Assert + Assert.AreEqual(firstCall, secondCall, "Multiple calls to DiscoverTools should not change the result"); + Assert.IsTrue(firstCall.Contains("simple_tool"), "Should still contain tools from first discovery"); + } + + [TestMethod] + public void TestGetMetadataJsonEmptyRegistry() + { + // Note: This test might not work in isolation due to static nature + // But it tests the exception handling + try + { + // This should work even with empty tools if the registry has been initialized + string metadataJson = McpToolRegistry.GetToolMetadataJson(); + Assert.IsNotNull(metadataJson, "Metadata JSON should not be null even for empty registry"); + } + catch (Exception ex) + { + Assert.AreEqual("Impossible to build tools list.", ex.Message, "Should throw expected exception for empty tools"); + } + } + + // Tests for InvokeTool function + [TestMethod] + public void TestInvokeToolSimpleStringType() + { + // Arrange - Simulate how HandleMcpRequest creates the Hashtable for simple types + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create Hashtable as it would be created from JSON: {"value":"Laurent is the best"} + Hashtable arguments = new Hashtable(); + arguments.Add("value", "Laurent is the best"); + + // Act + string result = McpToolRegistry.InvokeTool("simple_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("Processed: Laurent is the best"), "Result should contain processed string"); + } + + [TestMethod] + public void TestInvokeToolPrimitiveIntType() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create Hashtable for primitive int: {"value":"42"} + Hashtable arguments = new Hashtable(); + arguments.Add("value", "42"); // JSON numbers come as strings + + // Act + string result = McpToolRegistry.InvokeTool("primitive_int_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("84"), "Result should contain doubled value (42 * 2 = 84)"); + } + + [TestMethod] + public void TestInvokeToolPrimitiveBoolType() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create Hashtable for primitive bool: {"value":"true"} + Hashtable arguments = new Hashtable(); + arguments.Add("value", "true"); + + // Act + string result = McpToolRegistry.InvokeTool("primitive_bool_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("false"), "Result should contain inverted boolean (true -> false)"); + } + + [TestMethod] + public void TestInvokeToolComplexType() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create Hashtable for complex type: {"Name":"John","Value":"100"} + Hashtable arguments = new Hashtable(); + arguments.Add("Name", "John"); + arguments.Add("Value", "100"); + + // Act + string result = McpToolRegistry.InvokeTool("complex_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("John_processed"), "Result should contain processed name"); + Assert.IsTrue(result.Contains("200"), "Result should contain doubled value (100 * 2 = 200)"); + } + + [TestMethod] + public void TestInvokeToolNestedComplexType() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create Hashtable for nested type - simulating the JSON structure: + // {"Title":"Test","IsEnabled":"true","Details":"{\"Name\":\"John\",\"Value\":\"50\"}"} + Hashtable arguments = new Hashtable(); + arguments.Add("Title", "Test Title"); + arguments.Add("IsEnabled", "true"); + arguments.Add("Details", "{\"Name\":\"John\",\"Value\":\"50\"}"); // Nested object as JSON string + + // Act + string result = McpToolRegistry.InvokeTool("nested_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("Test Title"), "Result should contain the title"); + Assert.IsTrue(result.Contains("John"), "Result should contain nested name"); + Assert.IsTrue(result.Contains("50"), "Result should contain nested value"); + Assert.IsTrue(result.Contains("Enabled: True"), "Result should contain enabled status"); + } + + [TestMethod] + public void TestInvokeToolNoParameters() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create empty Hashtable for no parameters + Hashtable arguments = new Hashtable(); + + // Act + string result = McpToolRegistry.InvokeTool("no_param_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("No parameters required"), "Result should contain expected message"); + } + + [TestMethod] + public void TestInvokeToolNonExistentTool() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + Hashtable arguments = new Hashtable(); + arguments.Add("value", "test"); + + // Act & Assert + Assert.ThrowsException(typeof(Exception), () => + { + McpToolRegistry.InvokeTool("non_existent_tool", arguments); + }, "Should throw exception for non-existent tool"); + } + + [TestMethod] + public void TestInvokeToolWithNullArguments() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Act + string result = McpToolRegistry.InvokeTool("no_param_tool", null); + + // Assert + Assert.IsNotNull(result, "Result should not be null even with null arguments"); + } + + [TestMethod] + public void TestInvokeToolPrimitiveTypeConversions() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Test different string representations of numbers + Hashtable arguments1 = new Hashtable(); + arguments1.Add("value", "0"); + string result1 = McpToolRegistry.InvokeTool("primitive_int_tool", arguments1); + Assert.IsTrue(result1.Contains("0"), "Should handle zero correctly"); + + Hashtable arguments2 = new Hashtable(); + arguments2.Add("value", "-10"); + string result2 = McpToolRegistry.InvokeTool("primitive_int_tool", arguments2); + Assert.IsTrue(result2.Contains("-20"), "Should handle negative numbers correctly"); + + // Test boolean variations + Hashtable arguments3 = new Hashtable(); + arguments3.Add("value", "false"); + string result3 = McpToolRegistry.InvokeTool("primitive_bool_tool", arguments3); + Assert.IsTrue(result3.Contains("true"), "Should handle false -> true conversion"); + } + + [TestMethod] + public void TestInvokeToolComplexTypeWithMissingProperties() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + // Create Hashtable with only partial properties + Hashtable arguments = new Hashtable(); + arguments.Add("Name", "PartialJohn"); + // Missing Value property + + // Act + string result = McpToolRegistry.InvokeTool("complex_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsTrue(result.Contains("PartialJohn_processed"), "Should process available properties"); + // Should handle missing Value property gracefully (default to 0) + } + + [TestMethod] + public void TestInvokeToolReturnJsonSerialization() + { + // Arrange + Type[] toolTypes = new Type[] { typeof(TestToolsClass) }; + McpToolRegistry.DiscoverTools(toolTypes); + + Hashtable arguments = new Hashtable(); + arguments.Add("Name", "TestUser"); + arguments.Add("Value", "25"); + + // Act + string result = McpToolRegistry.InvokeTool("complex_tool", arguments); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + // Result should be valid JSON + Assert.IsTrue(result.StartsWith("{") || result.StartsWith("\""), "Result should be valid JSON"); + Assert.IsTrue(result.Contains("TestUser_processed"), "Should contain processed data"); + Assert.IsTrue(result.Contains("50"), "Should contain doubled value"); + } + } +} diff --git a/tests/McpServerTests/McpToolsAttributeTests.cs b/tests/McpServerTests/McpToolsAttributeTests.cs index 84a84bc..3952036 100644 --- a/tests/McpServerTests/McpToolsAttributeTests.cs +++ b/tests/McpServerTests/McpToolsAttributeTests.cs @@ -6,6 +6,26 @@ namespace McpServerTests { + // Test classes for complex type testing + public class SimpleTestClass + { + public string Name { get; set; } + public int Age { get; set; } + } + + public class ComplexTestClass + { + public string Title { get; set; } + public bool IsActive { get; set; } + public double Score { get; set; } + public SimpleTestClass NestedObject { get; set; } + } + + public class EmptyTestClass + { + // No properties + } + [TestClass] public class TestMcpToolsAttributeTests { @@ -14,11 +34,361 @@ public void TestSimpleTypeString() { // Arrange Type inputType = typeof(string); - string expectedJson = "[{\"name\":\"value\",\"type\":\"string\",\"description\":\"value\"}]"; + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\",\"description\":\"Input parameter of type String\"}},\"required\":[]}"; // Act string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); // Assert Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple string type does not match the expected output."); } + + [TestMethod] + public void TestSimpleTypeInt() + { + // Arrange + Type inputType = typeof(int); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"number\",\"description\":\"Input parameter of type Int32\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple int type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeBool() + { + // Arrange + Type inputType = typeof(bool); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"boolean\",\"description\":\"Input parameter of type Boolean\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple bool type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeDouble() + { + // Arrange + Type inputType = typeof(double); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"number\",\"description\":\"Input parameter of type Double\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple double type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeFloat() + { + // Arrange + Type inputType = typeof(float); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"number\",\"description\":\"Input parameter of type Single\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple float type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeLong() + { + // Arrange + Type inputType = typeof(long); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"number\",\"description\":\"Input parameter of type Int64\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple long type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeByte() + { + // Arrange + Type inputType = typeof(byte); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"number\",\"description\":\"Input parameter of type Byte\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple byte type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeShort() + { + // Arrange + Type inputType = typeof(short); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"number\",\"description\":\"Input parameter of type Int16\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple short type does not match the expected output."); + } + + [TestMethod] + public void TestSimpleTypeChar() + { + // Arrange + Type inputType = typeof(char); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\",\"description\":\"Input parameter of type Char\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple char type does not match the expected output."); + } + + [TestMethod] + public void TestComplexTypeSimple() + { + // Arrange + Type inputType = typeof(SimpleTestClass); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"Name\":{\"type\":\"string\",\"description\":\"Name\"},\"Age\":{\"type\":\"number\",\"description\":\"Age\"}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a simple complex type does not match the expected output."); + } + + [TestMethod] + public void TestComplexTypeNested() + { + // Arrange + Type inputType = typeof(ComplexTestClass); + string expectedJson = "{\"type\":\"object\",\"properties\":{\"Title\":{\"type\":\"string\",\"description\":\"Title\"},\"IsActive\":{\"type\":\"boolean\",\"description\":\"IsActive\"},\"Score\":{\"type\":\"number\",\"description\":\"Score\"},\"NestedObject\":{\"type\":\"object\",\"description\":\"NestedObject\",\"properties\":{\"Name\":{\"type\":\"string\",\"description\":\"Name\"},\"Age\":{\"type\":\"number\",\"description\":\"Age\"}}}},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for a nested complex type does not match the expected output."); + } + + [TestMethod] + public void TestComplexTypeEmpty() + { + // Arrange + Type inputType = typeof(EmptyTestClass); + string expectedJson = "{\"type\":\"object\",\"properties\":{},\"required\":[]}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated JSON for an empty complex type does not match the expected output."); + } + + [TestMethod] + public void TestNullInputType() + { + // Arrange + Type inputType = null; + // Act & Assert + Assert.ThrowsException(typeof(NullReferenceException), () => + { + nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateInputJson(inputType); }, "GenerateInputJson should throw an exception for null input type."); + } + + // Tests for GenerateOutputJson function + [TestMethod] + public void TestOutputJsonSimpleTypeString() + { + // Arrange + Type outputType = typeof(string); + string description = "Test string output"; + string expectedJson = "{\"type\":\"string\",\"description\":\"Test string output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a simple string type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonSimpleTypeInt() + { + // Arrange + Type outputType = typeof(int); + string description = "Test integer output"; + string expectedJson = "{\"type\":\"number\",\"description\":\"Test integer output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a simple int type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonSimpleTypeBool() + { + // Arrange + Type outputType = typeof(bool); + string description = "Test boolean output"; + string expectedJson = "{\"type\":\"boolean\",\"description\":\"Test boolean output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a simple bool type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonSimpleTypeDouble() + { + // Arrange + Type outputType = typeof(double); + string description = "Test double output"; + string expectedJson = "{\"type\":\"number\",\"description\":\"Test double output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a simple double type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonComplexTypeSimple() + { + // Arrange + Type outputType = typeof(SimpleTestClass); + string description = "Test simple complex output"; + string expectedJson = "{\"type\":\"object\",\"description\":\"Test simple complex output\",\"properties\":{\"Name\":{\"type\":\"string\",\"description\":\"Name\"},\"Age\":{\"type\":\"number\",\"description\":\"Age\"}}}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a simple complex type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonComplexTypeNested() + { + // Arrange + Type outputType = typeof(ComplexTestClass); + string description = "Test nested complex output"; + string expectedJson = "{\"type\":\"object\",\"description\":\"Test nested complex output\",\"properties\":{\"Title\":{\"type\":\"string\",\"description\":\"Title\"},\"IsActive\":{\"type\":\"boolean\",\"description\":\"IsActive\"},\"Score\":{\"type\":\"number\",\"description\":\"Score\"},\"NestedObject\":{\"type\":\"object\",\"description\":\"NestedObject\",\"properties\":{\"Name\":{\"type\":\"string\",\"description\":\"Name\"},\"Age\":{\"type\":\"number\",\"description\":\"Age\"}}}}}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a nested complex type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonComplexTypeEmpty() + { + // Arrange + Type outputType = typeof(EmptyTestClass); + string description = "Test empty complex output"; + string expectedJson = "{\"type\":\"object\",\"description\":\"Test empty complex output\",\"properties\":{}}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for an empty complex type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonWithEmptyDescription() + { + // Arrange + Type outputType = typeof(string); + string description = ""; + string expectedJson = "{\"type\":\"string\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON with empty description does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonWithNullDescription() + { + // Arrange + Type outputType = typeof(int); + string description = null; + string expectedJson = "{\"type\":\"number\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON with null description does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonNullOutputType() + { + // Arrange + Type outputType = null; + string description = "Test description"; + // Act & Assert + Assert.ThrowsException(typeof(NullReferenceException), () => + { + nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); }, "GenerateOutputJson should throw an exception for null output type."); + } + + [TestMethod] + public void TestOutputJsonArrayType() + { + // Arrange + Type outputType = typeof(string[]); + string description = "Test array output"; + string expectedJson = "{\"type\":\"array\",\"description\":\"Test array output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for an array type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonIntArrayType() + { + // Arrange + Type outputType = typeof(int[]); + string description = "Test integer array output"; + string expectedJson = "{\"type\":\"array\",\"description\":\"Test integer array output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for an integer array type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonLongType() + { + // Arrange + Type outputType = typeof(long); + string description = "Test long output"; + string expectedJson = "{\"type\":\"number\",\"description\":\"Test long output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a long type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonFloatType() + { + // Arrange + Type outputType = typeof(float); + string description = "Test float output"; + string expectedJson = "{\"type\":\"number\",\"description\":\"Test float output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a float type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonByteType() + { + // Arrange + Type outputType = typeof(byte); + string description = "Test byte output"; + string expectedJson = "{\"type\":\"number\",\"description\":\"Test byte output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a byte type does not match the expected output."); + } + + [TestMethod] + public void TestOutputJsonShortType() + { + // Arrange + Type outputType = typeof(short); + string description = "Test short output"; + string expectedJson = "{\"type\":\"number\",\"description\":\"Test short output\"}"; + // Act + string resultJson = nanoFramework.WebServer.Mcp.McpToolJsonHelper.GenerateOutputJson(outputType, description); + // Assert + Assert.AreEqual(expectedJson, resultJson, "The generated output JSON for a short type does not match the expected output."); + } } } diff --git a/tests/McpServerTests/McpToolsClasses.cs b/tests/McpServerTests/McpToolsClasses.cs deleted file mode 100644 index 1a63b05..0000000 --- a/tests/McpServerTests/McpToolsClasses.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using nanoFramework.WebServer.Mcp; - -namespace McpServerTests -{ - public class Person - { - public string Name { get; set; } - public string Surname { get; set; } - public int Age { get; set; } = 30; // Default age - public Address Address { get; set; } = new Address(); // Default address - } - - public class Address - { - public string Street { get; set; } = "Unknown"; - public string City { get; set; } = "Unknown"; - public string PostalCode { get; set; } = "00000"; - public string Country { get; set; } = "Unknown"; - } - - public class McpTools - { - [McpServerTool("process_person", "Processes a person object.")] - public static string ProcessPerson(Person person) - { - return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}"; - } - } -} diff --git a/tests/McpServerTests/packages.config b/tests/McpServerTests/packages.config index 28d330b..79818d8 100644 --- a/tests/McpServerTests/packages.config +++ b/tests/McpServerTests/packages.config @@ -1,5 +1,12 @@  + + + + + + + \ No newline at end of file diff --git a/tests/McpServerTests/packages.lock.json b/tests/McpServerTests/packages.lock.json index 2137220..a5f122b 100644 --- a/tests/McpServerTests/packages.lock.json +++ b/tests/McpServerTests/packages.lock.json @@ -8,6 +8,48 @@ "resolved": "1.17.11", "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.32, 1.11.32]", + "resolved": "1.11.32", + "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA==" + }, + "nanoFramework.System.Collections": { + "type": "Direct", + "requested": "[1.5.67, 1.5.67]", + "resolved": "1.5.67", + "contentHash": "MjSipUB70vrxjqTm1KfKTUqqjd0wbweiNyYFXONi0XClrH6HXsuX2lhDqXM8NWuYnWyYOqx8y20sXbvsH+4brg==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.43, 1.11.43]", + "resolved": "1.11.43", + "contentHash": "USwz59gxcNUzsiXfQohWSi8ANNwGDsp+qG4zBtHZU3rKMtvTsLI3rxdfMC77VehKqsCPn7aK3PU2oCRFo+1Rgg==" + }, + "nanoFramework.System.Net.Http.Server": { + "type": "Direct", + "requested": "[1.5.196, 1.5.196]", + "resolved": "1.5.196", + "contentHash": "cjr5Rj39duOjGcyvo/LMFdoeTeLg0zpFgFB7wJUXw0+65EiENEnJwqqR1CfbJEvBBpBMJdH/yLkK/8DU8Jk3XQ==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, "nanoFramework.TestFramework": { "type": "Direct", "requested": "[3.0.77, 3.0.77]", From c9a909590b6718f80433781da30f869295a84661 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 26 Jun 2025 22:34:11 +0200 Subject: [PATCH 4/8] adding build and config --- azure-pipelines.yml | 8 +++-- nanoFramework.WebServer.Mcp.nuspec | 36 +++++++++++++++++++ .../Properties/AssemblyInfo.cs | 32 +++-------------- .../nanoFramework.WebServer.Mcp.nfproj | 20 +++++++++++ 4 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 nanoFramework.WebServer.Mcp.nuspec diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3afe3f5..06eee73 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,7 +55,7 @@ steps: parameters: sonarCloudProject: 'nanoframework_lib-nanoframework.WebServer' - # build the 2 libs step + # build the 3 libs step - template: azure-pipelines-templates/class-lib-package.yml@templates parameters: nugetPackageName: 'nanoFramework.WebServer' @@ -64,7 +64,11 @@ steps: parameters: nugetPackageName: 'nanoFramework.WebServer.FileSystem' - # publish the 2 libs + - template: azure-pipelines-templates/class-lib-package.yml@templates + parameters: + nugetPackageName: 'nanoFramework.WebServer.Mcp' + + # publish the 3 libs - template: azure-pipelines-templates/class-lib-publish.yml@templates # create GitHub release build from main branch diff --git a/nanoFramework.WebServer.Mcp.nuspec b/nanoFramework.WebServer.Mcp.nuspec new file mode 100644 index 0000000..f56cce4 --- /dev/null +++ b/nanoFramework.WebServer.Mcp.nuspec @@ -0,0 +1,36 @@ + + + + nanoFramework.WebServer.Mcp + nanoFramework.WebServer.Mcp + $version$ + Laurent Ellerbach,nanoframework + false + LICENSE.md + + + docs\README.md + false + https://github.com/nanoframework/nanoFramework.WebServer + images\nf-logo.png + + Copyright (c) .NET Foundation and Contributors + This is a simple Model Context Protocol (MCP) multithread WebServer supporting tools for integration with AI Agents. +Perfect for .NET nanoFramework to be integrated with AI solutions. Supports both HTTPS and HTTP, Authentication, Strong Typing, Automatic Discovery. +This comes also with the nanoFramework WebServer. Allowing to create a REST API based project with ease as well. + + http https webserver net netmf nf nanoframework mcp ai agent model context protocol + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs b/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs index da9340e..ce37bbc 100644 --- a/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs +++ b/nanoFramework.WebServer.Mcp/Properties/AssemblyInfo.cs @@ -8,35 +8,13 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("CSharp.TestApplication")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("CSharp.TestApplication")] -[assembly: AssemblyCopyright("Copyright © 2025")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] +[assembly: AssemblyTitle("nanoFramework.WebServer.Mcp")] +[assembly: AssemblyCompany("nanoFramework Contributors")] +[assembly: AssemblyProduct("nanoFramework.WebServer.Mcp")] +[assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")] + // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] - -///////////////////////////////////////////////////////////////// -// This attribute is mandatory when building Interop libraries // -// update this whenever the native assembly signature changes // -[assembly: AssemblyNativeVersion("1.0.0.0")] -///////////////////////////////////////////////////////////////// diff --git a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj index ef9e4f0..e76ccf7 100644 --- a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj +++ b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj @@ -15,6 +15,18 @@ nanoFramework.WebServer.Mcp nanoFramework.WebServer.Mcp v1.0 + bin\$(Configuration)\nanoFramework.WebServer.Mcp.xml + true + true + + + true + + + ..\key.snk + + + false @@ -76,4 +88,12 @@ + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + \ No newline at end of file From abcd2ea9b8372ef7a00d2795e090f1c9240aaa68 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 26 Jun 2025 22:46:01 +0200 Subject: [PATCH 5/8] adding missing json nuget in nuspec --- nanoFramework.WebServer.Mcp.nuspec | 1 + 1 file changed, 1 insertion(+) diff --git a/nanoFramework.WebServer.Mcp.nuspec b/nanoFramework.WebServer.Mcp.nuspec index f56cce4..6a4eb63 100644 --- a/nanoFramework.WebServer.Mcp.nuspec +++ b/nanoFramework.WebServer.Mcp.nuspec @@ -23,6 +23,7 @@ This comes also with the nanoFramework WebServer. Allowing to create a REST API + From ef792f1a0c474cead10f73f22ccfb4b6809c4ff2 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 26 Jun 2025 22:52:14 +0200 Subject: [PATCH 6/8] removing runtime native --- .../nanoFramework.WebServer.Mcp.nfproj | 3 --- nanoFramework.WebServer.Mcp/packages.config | 1 - nanoFramework.WebServer.Mcp/packages.lock.json | 6 ------ 3 files changed, 10 deletions(-) diff --git a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj index e76ccf7..603621a 100644 --- a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj +++ b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj @@ -51,9 +51,6 @@ ..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll - - ..\packages\nanoFramework.Runtime.Native.1.7.11\lib\nanoFramework.Runtime.Native.dll - ..\packages\nanoFramework.System.Collections.1.5.67\lib\nanoFramework.System.Collections.dll diff --git a/nanoFramework.WebServer.Mcp/packages.config b/nanoFramework.WebServer.Mcp/packages.config index 82b7f1d..dd917ed 100644 --- a/nanoFramework.WebServer.Mcp/packages.config +++ b/nanoFramework.WebServer.Mcp/packages.config @@ -3,7 +3,6 @@ - diff --git a/nanoFramework.WebServer.Mcp/packages.lock.json b/nanoFramework.WebServer.Mcp/packages.lock.json index 3480499..513c863 100644 --- a/nanoFramework.WebServer.Mcp/packages.lock.json +++ b/nanoFramework.WebServer.Mcp/packages.lock.json @@ -20,12 +20,6 @@ "resolved": "1.11.32", "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA==" }, - "nanoFramework.Runtime.Native": { - "type": "Direct", - "requested": "[1.7.11, 1.7.11]", - "resolved": "1.7.11", - "contentHash": "XPSTltZ9KeBruogVmjQpCphi1nLoJH49mpyp2eGBs8BTjKuL5TkMO20MoI8r73F/PW5AppTq49HvIZZavU5nPQ==" - }, "nanoFramework.System.Collections": { "type": "Direct", "requested": "[1.5.67, 1.5.67]", From e3113fadcda6b8b0a1808ff49afc1fce8c83ea74 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 26 Jun 2025 22:56:20 +0200 Subject: [PATCH 7/8] adding dev container readme --- .devcontainer/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .devcontainer/README.md diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000..ee86a00 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,5 @@ +# .NET 10 DevContainer + +This is a .NET preview dev container. This has been used to work with the [McpClientTest](../tests/McpClientTest/) application. + +It is stronly encourage to update the preview version once .NET will be released. \ No newline at end of file From 48fa56a80963435d94ce75a6ef0ffc545b1aa3a1 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Fri, 27 Jun 2025 08:47:02 +0200 Subject: [PATCH 8/8] adding back wifi for compiling reasons --- .gitignore | 1 - tests/McpEndToEndTest/WiFi.cs | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/McpEndToEndTest/WiFi.cs diff --git a/.gitignore b/.gitignore index c8a7c0e..d5b88aa 100644 --- a/.gitignore +++ b/.gitignore @@ -258,5 +258,4 @@ paket-files/ #VSCode .vscode -tests/McpEndToEndTest/WiFi.cs **/.env \ No newline at end of file diff --git a/tests/McpEndToEndTest/WiFi.cs b/tests/McpEndToEndTest/WiFi.cs new file mode 100644 index 0000000..21b2965 --- /dev/null +++ b/tests/McpEndToEndTest/WiFi.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace McpEndToEndTest +{ + public partial class Program + { + private const string Ssid = "yourSSID"; + private const string Password = "YourPassword"; + } +}