Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,5 @@ paket-files/

#VSCode
.vscode
tests/McpEndToEndTest/WiFi.cs
**/.env
347 changes: 345 additions & 2 deletions README.md

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions nanoFramework.WebServer.Mcp.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>nanoFramework.WebServer.Mcp</id>
<title>nanoFramework.WebServer.Mcp</title>
<version>$version$</version>
<authors>Laurent Ellerbach,nanoframework</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="file">LICENSE.md</license>
<releaseNotes>
</releaseNotes>
<readme>docs\README.md</readme>
<developmentDependency>false</developmentDependency>
<projectUrl>https://github.com/nanoframework/nanoFramework.WebServer</projectUrl>
<icon>images\nf-logo.png</icon>
<repository type="git" url="https://github.com/nanoframework/nanoFramework.WebServer" commit="$commit$" />
<copyright>Copyright (c) .NET Foundation and Contributors</copyright>
<description>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.
</description>
<tags>http https webserver net netmf nf nanoframework mcp ai agent model context protocol</tags>
<dependencies>
<dependency id="nanoFramework.CoreLibrary" version="1.17.11" />
<dependency id="nanoFramework.System.Net.Http.Server" version="1.5.196" />
</dependencies>
</metadata>
<files>
<file src="nanoFramework.WebServer\bin\Release\nanoFramework.WebServer.*" target="lib" />
<file src="nanoFramework.WebServer.Mcp\bin\Release\nanoFramework.WebServer.Mcp.*" target="lib" />
<file src="assets\readme.txt" target="" />
<file src="README.md" target="docs\" />
<file src="assets\nf-logo.png" target="images" />
<file src="LICENSE.md" target="" />
</files>
</package>
27 changes: 27 additions & 0 deletions nanoFramework.WebServer.Mcp/DescriptionAttribute.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Specifies a description for a class member, such as a property or method, for use in documentation or metadata.
/// </summary>
public class DescriptionAttribute : Attribute
{
/// <summary>
/// Gets the description text associated with the member.
/// </summary>
public string Description { get; }

/// <summary>
/// Initializes a new instance of the <see cref="DescriptionAttribute"/> class with the specified description.
/// </summary>
/// <param name="description">The description text to associate with the member.</param>
public DescriptionAttribute(string description)
{
Description = description;
}
}
}
24 changes: 24 additions & 0 deletions nanoFramework.WebServer.Mcp/HashtableExtension.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools.
/// This controller is specifically designed for basic (user, password) authentication.
/// </summary>
[Authentication("Basic")]
public class McpServerBasicAuthenticationController : McpServerController
{
}
}
138 changes: 138 additions & 0 deletions nanoFramework.WebServer.Mcp/McpServerController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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
{
/// <summary>
/// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools.
/// </summary>
public class McpServerController
{
/// <summary>
/// The supported version of the MCP protocol.
/// </summary>
public const string SupportedVersion = "2025-03-26";

/// <summary>
/// Gets or sets the server name.
/// </summary>
public static string ServerName { get; set; } = "nanoFramework";

/// <summary>
/// Gets or sets the server version.
/// </summary>
public static string ServerVersion { get; set; } = "1.0.0";

/// <summary>
/// Gets or sets the instructions for using the MCP server.
/// </summary>
public static string Instructions { get; set; } = "This is an embedded device and only 1 request at a time should be sent.";

/// <summary>
/// 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.
/// </summary>
/// <param name="e">The web server event arguments containing the HTTP context and request/response information.</param>
[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));

// 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")
{
// 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\":\"{ServerName}\",\"version\":\"{ServerVersion}\"}}");

// Add instructions
sb.Append($",\"instructions\":\"{Instructions}\"}}}}");
}
else if (request["method"].ToString() == "tools/list")
{
// This is a request for the list of tools
string toolListJson = McpToolRegistry.GetToolMetadataJson();
sb.Append($",\"result\":{{{toolListJson}}}}}");
}
else if (request["method"].ToString() == "tools/call")
{
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);

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;
}
}
catch (Exception ex)
{
WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"id\":{id},\"error\":{{\"code\":-32602,\"message\":\"{ex.Message}\"}}}}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools.
/// This controller is specifically designed for key-based authentication.
/// </summary>
[Authentication("ApiKey")]
public class McpServerKeyAuthenticationController : McpServerController
{
}
}
45 changes: 45 additions & 0 deletions nanoFramework.WebServer.Mcp/McpServerToolAttribute.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Attribute to mark a method as an MCP server tool and provide metadata for discovery and documentation.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class McpServerToolAttribute : Attribute
{
/// <summary>
/// Gets the unique name of the tool.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the description of the tool.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the description of the tool's output.
/// </summary>
public string OutputDescription { get; }

/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolAttribute"/> class with the specified name, description, and output description.
/// </summary>
/// <param name="name">The unique name of the tool.</param>
/// <param name="description">The description of the tool.</param>
/// <param name="outputDescription">The description of the tool's output.</param>
public McpServerToolAttribute(string name, string description = "", string outputDescription = "")
{
Name = name;
Description = description;
OutputDescription = outputDescription;
}
}
}
Loading
Loading