Skip to content

Commit 7233f8d

Browse files
authored
Add Model Context Protocol for AI Agent support (#333)
1 parent b8d2869 commit 7233f8d

36 files changed

+3109
-4
lines changed

.devcontainer/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# .NET 10 DevContainer
2+
3+
This is a .NET preview dev container. This has been used to work with the [McpClientTest](../tests/McpClientTest/) application.
4+
5+
It is stronly encourage to update the preview version once .NET will be released.

.devcontainer/devcontainer.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "C# .NET 10 Preview",
3+
"image": "mcr.microsoft.com/devcontainers/dotnet:dev-10.0-preview-noble",
4+
"features": {
5+
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
6+
},
7+
"customizations": {
8+
"vscode": {
9+
"extensions": [
10+
"ms-dotnettools.csharp",
11+
"editorconfig.editorconfig",
12+
"GitHub.copilot-chat"
13+
]
14+
}
15+
},
16+
"postCreateCommand": "dotnet --version"
17+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,4 @@ paket-files/
258258

259259
#VSCode
260260
.vscode
261+
**/.env

README.md

Lines changed: 345 additions & 2 deletions
Large diffs are not rendered by default.

azure-pipelines.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ steps:
5555
parameters:
5656
sonarCloudProject: 'nanoframework_lib-nanoframework.WebServer'
5757

58-
# build the 2 libs step
58+
# build the 3 libs step
5959
- template: azure-pipelines-templates/class-lib-package.yml@templates
6060
parameters:
6161
nugetPackageName: 'nanoFramework.WebServer'
@@ -64,7 +64,11 @@ steps:
6464
parameters:
6565
nugetPackageName: 'nanoFramework.WebServer.FileSystem'
6666

67-
# publish the 2 libs
67+
- template: azure-pipelines-templates/class-lib-package.yml@templates
68+
parameters:
69+
nugetPackageName: 'nanoFramework.WebServer.Mcp'
70+
71+
# publish the 3 libs
6872
- template: azure-pipelines-templates/class-lib-publish.yml@templates
6973

7074
# create GitHub release build from main branch

nanoFramework.WebServer.Mcp.nuspec

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
3+
<metadata>
4+
<id>nanoFramework.WebServer.Mcp</id>
5+
<title>nanoFramework.WebServer.Mcp</title>
6+
<version>$version$</version>
7+
<authors>Laurent Ellerbach,nanoframework</authors>
8+
<requireLicenseAcceptance>false</requireLicenseAcceptance>
9+
<license type="file">LICENSE.md</license>
10+
<releaseNotes>
11+
</releaseNotes>
12+
<readme>docs\README.md</readme>
13+
<developmentDependency>false</developmentDependency>
14+
<projectUrl>https://github.com/nanoframework/nanoFramework.WebServer</projectUrl>
15+
<icon>images\nf-logo.png</icon>
16+
<repository type="git" url="https://github.com/nanoframework/nanoFramework.WebServer" commit="$commit$" />
17+
<copyright>Copyright (c) .NET Foundation and Contributors</copyright>
18+
<description>This is a simple Model Context Protocol (MCP) multithread WebServer supporting tools for integration with AI Agents.
19+
Perfect for .NET nanoFramework to be integrated with AI solutions. Supports both HTTPS and HTTP, Authentication, Strong Typing, Automatic Discovery.
20+
This comes also with the nanoFramework WebServer. Allowing to create a REST API based project with ease as well.
21+
</description>
22+
<tags>http https webserver net netmf nf nanoframework mcp ai agent model context protocol</tags>
23+
<dependencies>
24+
<dependency id="nanoFramework.CoreLibrary" version="1.17.11" />
25+
<dependency id="nanoFramework.System.Net.Http.Server" version="1.5.196" />
26+
<dependency id="nanoFramework.Json" version="2.2.199" />
27+
</dependencies>
28+
</metadata>
29+
<files>
30+
<file src="nanoFramework.WebServer\bin\Release\nanoFramework.WebServer.*" target="lib" />
31+
<file src="nanoFramework.WebServer.Mcp\bin\Release\nanoFramework.WebServer.Mcp.*" target="lib" />
32+
<file src="assets\readme.txt" target="" />
33+
<file src="README.md" target="docs\" />
34+
<file src="assets\nf-logo.png" target="images" />
35+
<file src="LICENSE.md" target="" />
36+
</files>
37+
</package>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace nanoFramework.WebServer.Mcp
7+
{
8+
/// <summary>
9+
/// Specifies a description for a class member, such as a property or method, for use in documentation or metadata.
10+
/// </summary>
11+
public class DescriptionAttribute : Attribute
12+
{
13+
/// <summary>
14+
/// Gets the description text associated with the member.
15+
/// </summary>
16+
public string Description { get; }
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="DescriptionAttribute"/> class with the specified description.
20+
/// </summary>
21+
/// <param name="description">The description text to associate with the member.</param>
22+
public DescriptionAttribute(string description)
23+
{
24+
Description = description;
25+
}
26+
}
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
5+
using System.Collections;
6+
7+
namespace nanoFramework.WebServer.Mcp
8+
{
9+
internal static class HashtableExtension
10+
{
11+
public static bool ContainsKey(this Hashtable hashtable, string key)
12+
{
13+
foreach (object k in hashtable.Keys)
14+
{
15+
if (k is string strKey && strKey.Equals(key))
16+
{
17+
return true;
18+
}
19+
}
20+
21+
return false;
22+
}
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace nanoFramework.WebServer.Mcp
5+
{
6+
/// <summary>
7+
/// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools.
8+
/// This controller is specifically designed for basic (user, password) authentication.
9+
/// </summary>
10+
[Authentication("Basic")]
11+
public class McpServerBasicAuthenticationController : McpServerController
12+
{
13+
}
14+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Text;
9+
using nanoFramework.Json;
10+
11+
namespace nanoFramework.WebServer.Mcp
12+
{
13+
/// <summary>
14+
/// McpServerController class provides endpoints for handling requests related to MCP (Model Context Protocol) tools.
15+
/// </summary>
16+
public class McpServerController
17+
{
18+
/// <summary>
19+
/// The supported version of the MCP protocol.
20+
/// </summary>
21+
public const string SupportedVersion = "2025-03-26";
22+
23+
/// <summary>
24+
/// Gets or sets the server name.
25+
/// </summary>
26+
public static string ServerName { get; set; } = "nanoFramework";
27+
28+
/// <summary>
29+
/// Gets or sets the server version.
30+
/// </summary>
31+
public static string ServerVersion { get; set; } = "1.0.0";
32+
33+
/// <summary>
34+
/// Gets or sets the instructions for using the MCP server.
35+
/// </summary>
36+
public static string Instructions { get; set; } = "This is an embedded device and only 1 request at a time should be sent.";
37+
38+
/// <summary>
39+
/// Handles POST requests to the "mcp" route.
40+
/// Processes the incoming request, invokes the specified tool with provided parameters, and writes the result to the response stream in JSON format.
41+
/// </summary>
42+
/// <param name="e">The web server event arguments containing the HTTP context and request/response information.</param>
43+
[Route("mcp"), Method("POST")]
44+
public void HandleMcpRequest(WebServerEventArgs e)
45+
{
46+
e.Context.Response.ContentType = "application/json";
47+
int id = 0;
48+
StringBuilder sb = new StringBuilder();
49+
50+
try
51+
{
52+
// Read the POST body from the request stream
53+
var requestStream = e.Context.Request.InputStream;
54+
byte[] buffer = new byte[requestStream.Length];
55+
requestStream.Read(buffer, 0, buffer.Length);
56+
string requestBody = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
57+
58+
Debug.WriteLine($"Request Body: {requestBody}");
59+
60+
Hashtable request = (Hashtable)JsonConvert.DeserializeObject(requestBody, typeof(Hashtable));
61+
62+
// Sets jsonrpc version
63+
sb.Append("{\"jsonrpc\": \"2.0\"");
64+
// Check if we have an id if yes, add it to the answer
65+
if (request.ContainsKey("id"))
66+
{
67+
id = Convert.ToInt32(request["id"].ToString());
68+
sb.Append($",\"id\":{id}");
69+
}
70+
71+
if (request.ContainsKey("method"))
72+
{
73+
// Case the server us initilaized
74+
if (request["method"].ToString() == "notifications/initialized")
75+
{
76+
WebServer.OutputHttpCode(e.Context.Response, System.Net.HttpStatusCode.OK);
77+
return;
78+
}
79+
80+
if (request["method"].ToString() == "initialize")
81+
{
82+
// Check if client sent params with protocolVersion
83+
if (request.ContainsKey("params") && request["params"] is Hashtable initParams)
84+
{
85+
if (initParams.ContainsKey("protocolVersion"))
86+
{
87+
string clientVersion = initParams["protocolVersion"].ToString();
88+
if (clientVersion != SupportedVersion)
89+
{
90+
sb.Append($",\"error\":{{\"code\":-32602,\"message\":\"Unsupported protocol version\",\"data\":{{\"supported\":[\"{SupportedVersion}\"],\"requested\":\"{clientVersion}\"}}}}}}");
91+
WebServer.OutPutStream(e.Context.Response, sb.ToString());
92+
return;
93+
}
94+
}
95+
}
96+
97+
sb.Append($",\"result\":{{\"protocolVersion\":\"{SupportedVersion}\"");
98+
99+
// Add capabilities
100+
sb.Append($",\"capabilities\":{{\"logging\":{{}},\"prompts\":{{\"listChanged\":false}},\"resources\":{{\"subscribe\":false,\"listChanged\":false}},\"tools\":{{\"listChanged\":false}}}}");
101+
102+
// Add serverInfo
103+
sb.Append($",\"serverInfo\":{{\"name\":\"{ServerName}\",\"version\":\"{ServerVersion}\"}}");
104+
105+
// Add instructions
106+
sb.Append($",\"instructions\":\"{Instructions}\"}}}}");
107+
}
108+
else if (request["method"].ToString() == "tools/list")
109+
{
110+
// This is a request for the list of tools
111+
string toolListJson = McpToolRegistry.GetToolMetadataJson();
112+
sb.Append($",\"result\":{{{toolListJson}}}}}");
113+
}
114+
else if (request["method"].ToString() == "tools/call")
115+
{
116+
string toolName = ((Hashtable)request["params"])["name"].ToString();
117+
Hashtable param = ((Hashtable)request["params"])["arguments"] == null ? null : (Hashtable)((Hashtable)request["params"])["arguments"];
118+
119+
string result = McpToolRegistry.InvokeTool(toolName, param);
120+
121+
sb.Append($",\"result\":{{\"content\":[{{\"type\":\"text\",\"text\":{result}}}]}}}}");
122+
}
123+
else
124+
{
125+
sb.Append($",\"error\":{{\"code\":-32601,\"message\":\"Method not found\"}}}}");
126+
}
127+
128+
WebServer.OutPutStream(e.Context.Response, sb.ToString());
129+
return;
130+
}
131+
}
132+
catch (Exception ex)
133+
{
134+
WebServer.OutPutStream(e.Context.Response, $"{{\"jsonrpc\":\"2.0\",\"id\":{id},\"error\":{{\"code\":-32602,\"message\":\"{ex.Message}\"}}}}");
135+
}
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)