diff --git a/README.md b/README.md
index 70f0115..0e7a4f8 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-----
-### Welcome to the .NET **nanoFramework** WebServer repository including Model Context Protocol (MCP)
+# .NET nanoFramework WebServer with Model Context Protocol (MCP)
## Build status
@@ -14,251 +14,42 @@
| nanoFramework.WebServer.FileSystem | [](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65&repoName=nanoframework%2FnanoFramework.WebServer&branchName=main) | [](https://www.nuget.org/packages/nanoFramework.WebServer.FileSystem/) |
| nanoFramework.WebServer.Mcp | [](https://dev.azure.com/nanoframework/nanoFramework.WebServer/_build/latest?definitionId=65&repoName=nanoframework%2FnanoFramework.WebServer&branchName=main) | [](https://www.nuget.org/packages/nanoFramework.WebServer.Mcp/) |
-## .NET nanoFramework WebServer and Model Context Protocol (MCP) extension
+## Overview
-This library was coded by [Laurent Ellerbach](https://github.com/Ellerbach) who generously offered it to the .NET **nanoFramework** project.
+This library provides a lightweight, multi-threaded HTTP/HTTPS WebServer for .NET nanoFramework with comprehensive **Model Context Protocol (MCP)** support for AI agent integration.
-This is a simple nanoFramework WebServer. Features:
+### Key Features
-- Handle multi-thread requests
-- Serve static files from any storage using [`nanoFramework.WebServer.FileSystem` NuGet](https://www.nuget.org/packages/nanoFramework.WebServer.FileSystem). Requires a target device with support for storage (having `System.IO.FileSystem` capability).
-- Handle parameter in URL
-- Possible to have multiple WebServer running at the same time
-- supports GET/PUT and any other word
-- Supports any type of header
-- Supports content in POST
-- Reflection for easy usage of controllers and notion of routes
-- 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).
+- **Multi-threaded request handling**
+- **Static file serving** with FileSystem support
+- **RESTful API support** with parameter handling
+- **Route-based controllers** with attribute decoration
+- **Authentication support** (Basic, API Key)
+- **HTTPS/SSL support** with certificates
+- **Model Context Protocol (MCP)** for AI agent integration
+- **Automatic tool discovery** and JSON-RPC 2.0 compliance
-Limitations:
+## Quick Start
-- Does not support any zip in the request or response stream
+### Basic Event Based WebServer
-## Usage
-
-You just need to specify a port and a timeout for the queries and add an event handler when a request is incoming. With this first way, you will have an event raised every time you'll receive a request.
-
-```csharp
-using (WebServer server = new WebServer(80, HttpProtocol.Http)
-{
- // Add a handler for commands that are received by the server.
- server.CommandReceived += ServerCommandReceived;
-
- // Start the server.
- server.Start();
-
- Thread.Sleep(Timeout.Infinite);
-}
-```
-
-You can as well pass a controller where you can use decoration for the routes and method supported.
-
-```csharp
-using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest) }))
-{
- // Start the server.
- server.Start();
-
- Thread.Sleep(Timeout.Infinite);
-}
-```
-
-In this case, you're passing 2 classes where you have public methods decorated which will be called every time the route is found.
-
-With the previous example, a very simple and straight forward Test controller will look like that:
+Using the Web Server is very straight forward and supports event based calls.
```csharp
-public class ControllerTest
-{
- [Route("test"), Route("Test2"), Route("tEst42"), Route("TEST")]
- [CaseSensitive]
- [Method("GET")]
- public void RoutePostTest(WebServerEventArgs e)
- {
- string route = $"The route asked is {e.Context.Request.RawUrl.TrimStart('/').Split('/')[0]}";
- e.Context.Response.ContentType = "text/plain";
- WebServer.OutPutStream(e.Context.Response, route);
- }
-
- [Route("test/any")]
- public void RouteAnyTest(WebServerEventArgs e)
- {
- WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
- }
-}
-```
-
-In this example, the `RoutePostTest` will be called every time the called url will be `test` or `Test2` or `tEst42` or `TEST`, the url can be with parameters and the method GET. Be aware that `Test` won't call the function, neither `test/`.
-
-The `RouteAnyTest`is called whenever the url is `test/any` whatever the method is.
-
-There is a more advance example with simple REST API to get a list of Person and add a Person. Check it in the [sample](./WebServer.Sample/ControllerPerson.cs).
-
-> [!Important]
->
-> By default the routes are not case sensitive and the attribute **must** be lowercase.
-> If you want to use case sensitive routes like in the previous example, use the attribute `CaseSensitive`. As in the previous example, you **must** write the route as you want it to be responded to.
-
-## A simple GPIO controller REST API
-
-You will find in simple [GPIO controller sample](https://github.com/nanoframework/Samples/tree/main/samples/Webserver/WebServer.GpioRest) REST API. The controller not case sensitive and is working like this:
-
-- To open the pin 2 as output: http://yoururl/open/2/output
-- To open pin 4 as input: http://yoururl/open/4/input
-- To write the value high to pin 2: http://yoururl/write/2/high
- - You can use high or 1, it has the same effect and will place the pin in high value
- - You can use low of 0, it has the same effect and will place the pin in low value
-- To read the pin 4: http://yoururl/read/4, you will get as a raw text `high`or `low`depending on the state
-
-## Authentication on controllers
-
-Controllers support authentication. 3 types of authentications are currently implemented on controllers only:
-
-- Basic: the classic user and password following the HTTP standard. Usage:
- - `[Authentication("Basic")]` will use the default credential of the webserver
- - `[Authentication("Basic:myuser mypassword")]` will use myuser as a user and my password as a password. Note: the user cannot contains spaces.
-- APiKey in header: add ApiKey in headers with the API key. Usage:
- - `[Authentication("ApiKey")]` will use the default credential of the webserver
- - `[Authentication("ApiKeyc:akey")]` will use akey as ApiKey.
-- None: no authentication required. Usage:
- - `[Authentication("None")]` will use the default credential of the webserver
-
-The Authentication attribute applies to both public Classes an public Methods.
-
-As for the rest of the controller, you can add attributes to define them, override them. The following example gives an idea of what can be done:
-
-```csharp
-[Authentication("Basic")]
-class ControllerAuth
-{
- [Route("authbasic")]
- public void Basic(WebServerEventArgs e)
- {
- WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
- }
-
- [Route("authbasicspecial")]
- [Authentication("Basic:user2 password")]
- public void Special(WebServerEventArgs e)
- {
- WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
- }
-
- [Authentication("ApiKey:superKey1234")]
- [Route("authapi")]
- public void Key(WebServerEventArgs e)
- {
- WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
- }
-
- [Route("authnone")]
- [Authentication("None")]
- public void None(WebServerEventArgs e)
- {
- WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
- }
-
- [Authentication("ApiKey")]
- [Route("authdefaultapi")]
- public void DefaultApi(WebServerEventArgs e)
- {
- WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
- }
-}
-```
-
-And you can pass default credentials to the server:
+// You need to be connected to a wifi or ethernet connection with a proper IP Address
-```csharp
-using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ControllerPerson), typeof(ControllerTest), typeof(ControllerAuth) }))
+using (WebServer server = new WebServer(80, HttpProtocol.Http))
{
- // To test authentication with various scenarios
- server.ApiKey = "ATopSecretAPIKey1234";
- server.Credential = new NetworkCredential("topuser", "topPassword");
-
- // Start the server.
+ server.CommandReceived += ServerCommandReceived;
server.Start();
-
Thread.Sleep(Timeout.Infinite);
}
-```
-
-With the previous example the following happens:
-
-- All the controller by default, even when nothing is specified will use the controller credentials. In our case, the Basic authentication with the default user (topuser) and password (topPassword) will be used.
- - When calling http://yoururl/authbasic from a browser, you will be prompted for the user and password, use the default one topuser and topPassword to get access
- - When calling http://yoururl/authnone, you won't be prompted because the authentication has been overridden for no authentication
- - When calling http://yoururl/authbasicspecial, the user and password are different from the defautl ones, user2 and password is the right couple here
-- If you would have define in the controller a specific user and password like `[Authentication("Basic:myuser mypassword")]`, then the default one for all the controller would have been myuser and mypassword
-- When calling http://yoururl/authapi, you must pass the header `ApiKey` (case sensitive) with the value `superKey1234` to get authorized, this is overridden the default Basic authentication
-- When calling http://yoururl/authdefaultapi, the default key `ATopSecretAPIKey1234` will be used so you have to pass it in the headers of the request
-
-All up, this is an example to show how to use authentication, it's been defined to allow flexibility.
-
-The webserver supports having multiple authentication methods or credentials for the same route. Each pair of authentication method plus credentials should have its own method in the controller:
-
-```csharp
-class MixedController
-{
-
- [Route("sameroute")]
- [Authentication("Basic")]
- public void Basic(WebServerEventArgs e)
- {
- WebServer.OutPutStream(e.Context.Response, "sameroute: Basic");
- }
-
- [Authentication("ApiKey:superKey1234")]
- [Route("sameroute")]
- public void Key(WebServerEventArgs e)
- {
- WebServer.OutPutStream(e.Context.Response, "sameroute: API key #1");
- }
-
- [Authentication("ApiKey:superKey5678")]
- [Route("sameroute")]
- public void Key2(WebServerEventArgs e)
- {
- WebServer.OutPutStream(e.Context.Response, "sameroute: API key #2");
- }
-
- [Route("sameroute")]
- public void None(WebServerEventArgs e)
- {
- WebServer.OutPutStream(e.Context.Response, "sameroute: Public");
- }
-}
-```
-
-The webserver selects the route for a request:
-
-- If there are no matching methods, a not-found response (404) is returned.
-- If authentication information is passed in the header of the request, then only methods that require authentication are considered. If one of the method's credentials matches the credentials passed in the request, that method is called. Otherwise a non-authorized response (401) will be returned.
-- If no authentication information is passed in the header of the request:
- - If one of the methods does not require authentication, that method is called.
- - Otherwise a non-authorized response (401) will be returned. If one of the methods requires basic authentication, the `WWW-Authenticate` header is included to request credentials.
-
-The webserver does not support more than one matching method. Calling multiple methods most likely results in an exception as a subsequent method tries to modify a response that is already processed by the first method. The webserver does not know what to do and returns an internal server error (500). The body of the response lists the matching methods.
-
-Having multiple matching methods is considered a programming error. One way this occurs is if two methods in a controller accidentally have the same route. Returning an internal server error with the names of the methods makes it easy to discover the error. It is expected that the error is discovered and fixed in testing. Then the internal error will not occur in the application that is deployed to a device.
-
-## Managing incoming queries thru events
-
-Very basic usage is the following:
-```csharp
private static void ServerCommandReceived(object source, WebServerEventArgs e)
{
- var url = e.Context.Request.RawUrl;
- Debug.WriteLine($"Command received: {url}, Method: {e.Context.Request.HttpMethod}");
-
- if (url.ToLower() == "/sayhello")
+ if (e.Context.Request.RawUrl.ToLower() == "/hello")
{
- // This is simple raw text returned
- WebServer.OutPutStream(e.Context.Response, "It's working, url is empty, this is just raw text, /sayhello is just returning a raw text");
+ WebServer.OutPutStream(e.Context.Response, "Hello from nanoFramework!");
}
else
{
@@ -267,344 +58,84 @@ private static void ServerCommandReceived(object source, WebServerEventArgs e)
}
```
-You can do more advance scenario like returning a full HTML page:
-
-```csharp
-WebServer.OutPutStream(e.Context.Response, "
" +
- "Hi from nanoFramework ServerYou want me to say hello in a real HTML page!
Generate an internal text.txt file
" +
- "Download the Text.txt file
" +
- "Try this url with parameters: /param.htm?param1=42&second=24&NAme=Ellerbach");
-```
-
-And can get parameters from a URL a an example from the previous link on the param.html page:
-
-```csharp
-if (url.ToLower().IndexOf("/param.htm") == 0)
-{
- // Test with parameters
- var parameters = WebServer.decryptParam(url);
- string toOutput = "" +
- "Hi from nanoFramework ServerHere are the parameters of this URL:
";
- foreach (var par in parameters)
- {
- toOutput += $"Parameter name: {par.Name}, Value: {par.Value}
";
- }
- toOutput += "";
- WebServer.OutPutStream(e.Context.Response, toOutput);
-}
-```
+### Controller-Based WebServer
-And server static files:
+Controllers are supported including with parametarized routes like `api/led/{id}/dosomething/{order}`.
```csharp
-// E = USB storage
-// D = SD Card
-// I = Internal storage
-// Adjust this based on your configuration
-const string DirectoryPath = "I:\\";
-string[] _listFiles;
-
-// Gets the list of all files in a specific directory
-// See the MountExample for more details if you need to mount an SD card and adjust here
-// https://github.com/nanoframework/Samples/blob/main/samples/System.IO.FileSystem/MountExample/Program.cs
-_listFiles = Directory.GetFiles(DirectoryPath);
-// Remove the root directory
-for (int i = 0; i < _listFiles.Length; i++)
-{
- _listFiles[i] = _listFiles[i].Substring(DirectoryPath.Length);
-}
-
-var fileName = url.Substring(1);
-// Note that the file name is case sensitive
-// Very simple example serving a static file on an SD card
-foreach (var file in _listFiles)
+using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(MyController) }))
{
- if (file == fileName)
- {
- WebServer.SendFileOverHTTP(e.Context.Response, DirectoryPath + file);
- return;
- }
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
}
-WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
-```
-
-> [!Important]
->
-> Serving files requires the `nanoFramework.WebServer.FileSystem` nuget **AND** that the device supports storage so `System.IO.FileSystem`.
-
-And also **REST API** is supported, here is a comprehensive example:
-
-```csharp
-if (url.ToLower().IndexOf("/api/") == 0)
+public class MyController
{
- string ret = $"Your request type is: {e.Context.Request.HttpMethod}\r\n";
- ret += $"The request URL is: {e.Context.Request.RawUrl}\r\n";
- var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);
- if (parameters != null)
- {
- ret += "List of url parameters:\r\n";
- foreach (var param in parameters)
- {
- ret += $" Parameter name: {param.Name}, value: {param.Value}\r\n";
- }
- }
-
- if (e.Context.Request.Headers != null)
- {
- ret += $"Number of headers: {e.Context.Request.Headers.Count}\r\n";
- }
- else
- {
- ret += "There is no header in this request\r\n";
- }
-
- foreach (var head in e.Context.Request.Headers?.AllKeys)
+ [Route("api/hello")]
+ [Method("GET")]
+ public void Hello(WebServerEventArgs e)
{
- ret += $" Header name: {head}, Values:";
- var vals = e.Context.Request.Headers.GetValues(head);
- foreach (var val in vals)
- {
- ret += $"{val} ";
- }
-
- ret += "\r\n";
+ WebServer.OutPutStream(e.Context.Response, "Hello from Controller!");
}
- if (e.Context.Request.ContentLength64 > 0)
+ [Route("api/led/{id}")]
+ [Method("GET")]
+ public void LedState(WebServerEventArgs e)
{
-
- ret += $"Size of content: {e.Context.Request.ContentLength64}\r\n";
-
- var contentTypes = e.Context.Request.Headers?.GetValues("Content-Type");
- var isMultipartForm = contentTypes != null && contentTypes.Length > 0 && contentTypes[0].StartsWith("multipart/form-data;");
-
- if(isMultipartForm)
- {
- var form = e.Context.Request.ReadForm();
- ret += $"Received a form with {form.Parameters.Length} parameters and {form.Files.Length} files.";
- }
- else
- {
- var body = e.Context.Request.ReadBody();
-
- ret += $"Request body hex string representation:\r\n";
- for (int i = 0; i < body.Length; i++)
- {
- ret += body[i].ToString("X") + " ";
- }
- }
-
+ string ledId = e.GetRouteParameter("id");
+ WebServer.OutPutStream(e.Context.Response, $"You selected Led {ledId}!");
}
-
- WebServer.OutPutStream(e.Context.Response, ret);
-}
-```
-
-This API example is basic but as you get the method, you can choose what to do.
-
-As you get the url, you can check for a specific controller called. And you have the parameters and the content payload!
-
-Notice the extension methods to read the body of the request:
-
-- ReadBody will read the data from the InputStream while the data is flowing in which might be in multiple passes depending on the size of the body
-- ReadForm allows to read a multipart/form-data form and returns the text key/value pairs as well as any files in the request
-
-Example of a result with call:
-
-
-
-And more! Check the complete example for more about this WebServer!
-
-## Using HTTPS
-
-You will need to generate a certificate and keys:
-
-```csharp
-X509Certificate _myWebServerCertificate509 = new X509Certificate2(_myWebServerCrt, _myWebServerPrivateKey, "1234");
-
-// X509 RSA key PEM format 2048 bytes
- // generate with openssl:
- // > openssl req -newkey rsa:2048 -nodes -keyout selfcert.key -x509 -days 365 -out selfcert.crt
- // and paste selfcert.crt content below:
- private const string _myWebServerCrt =
-@"-----BEGIN CERTIFICATE-----
-MORETEXT
------END CERTIFICATE-----";
-
- // this one is generated with the command below. We need a password.
- // > openssl rsa -des3 -in selfcert.key -out selfcertenc.key
- // the one below was encoded with '1234' as the password.
- private const string _myWebServerPrivateKey =
-@"-----BEGIN RSA PRIVATE KEY-----
-MORETEXTANDENCRYPTED
------END RSA PRIVATE KEY-----";
-
-using (WebServer server = new WebServer(443, HttpProtocol.Https)
-{
- // Add a handler for commands that are received by the server.
- server.CommandReceived += ServerCommandReceived;
- server.HttpsCert = _myWebServerCertificate509;
-
- server.SslProtocols = System.Net.Security.SslProtocols.Tls | System.Net.Security.SslProtocols.Tls11 | System.Net.Security.SslProtocols.Tls12;
- // Start the server.
- server.Start();
-
- Thread.Sleep(Timeout.Infinite);
}
```
-> [!IMPORTANT]
-> Because the certificate above is not issued from a Certificate Authority it won't be recognized as a valid certificate. If you want to access the nanoFramework device with your browser, for example, you'll have to add the [CRT file](WebServer.Sample\webserver-cert.crt) as a trusted one. On Windows, you just have to double click on the CRT file and then click "Install Certificate...".
-
-You can of course use the routes as defined earlier. Both will work, event or route with the notion of controller.
-
-## WebServer status
-
-It is possible to subscribe to an event to get the WebServer status. That can be useful to restart the server, put in place a retry mechanism or equivalent.
-
-```csharp
-server.WebServerStatusChanged += WebServerStatusChanged;
-
-private static void WebServerStatusChanged(object obj, WebServerStatusEventArgs e)
-{
- // Do whatever you need like restarting the server
- Debug.WriteLine($"The web server is now {(e.Status == WebServerStatus.Running ? "running" : "stopped" )}");
-}
-```
-
-## E2E tests
-
-There is a collection of postman tests `nanoFramework WebServer E2E Tests.postman_collection.json` in WebServerE2ETests which should be used for testing WebServer in real world scenario. Usage is simple:
-- Import json file into Postman
-- Deploy WebServerE2ETests to your device - copy IP
-- Set the `base_url` variable to match your device IP address
-- Choose request you want to test or run whole collection and check tests results.
-
-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.
+Enable AI agents to interact with your embedded devices through standardized tools and JSON-RPC 2.0 protocol.
### 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
+public class IoTTools
{
- // 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)
+ [McpServerTool("read_sensor", "Reads temperature from sensor")]
+ public static string ReadTemperature()
{
- return $"Processed: {person.Name} {person.Surname}, Age: {person.Age}, Location: {person.Address.City}, {person.Address.Country}";
+ // Your sensor reading code
+ return "23.5°C";
}
- // Tool returning complex objects
- [McpServerTool("get_default_person", "Returns a default person object", "A person object with default values")]
- public Person GetDefaultPerson()
+ [McpServerTool("control_led", "Controls device LED", "Uutput the statusof the LED")]
+ public static string ControlLed(LedCommand command)
{
- return new Person
- {
- Name = "John",
- Surname = "Doe",
- Age = "30",
- Address = new Address
- {
- Street = "123 Main St",
- City = "Anytown",
- PostalCode = "12345",
- Country = "USA"
- }
- };
+ // Your LED control code
+ return $"LED set to {command.State}";
}
}
-```
-> [!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
+public class LedCommand
{
- [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";
+ [Description("LED state: on, off, or blink")]
+ public string State { get; set; }
}
```
### 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
+ // Connect to WiFi first
+ var connected = WifiNetworkHelper.ConnectDhcp(Ssid, Password, requiresDateTime: true);
+
+ // Discover and register MCP tools
+ McpToolRegistry.DiscoverTools(new Type[] { typeof(IoTTools) });
+
+ // Start WebServer with MCP support
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"
+ {
+ // Optional customization
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.";
+ McpServerController.Instructions = "IoT device with sensor and LED control capabilities.";
server.Start();
Thread.Sleep(Timeout.Infinite);
@@ -612,221 +143,59 @@ public static void Main()
}
```
-### 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.
+### AI Agent Integration
-#### Tool Discovery Request
+Once running, AI agents can discover and invoke your tools:
```json
+// Tool discovery
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"}
- }
- }
- }
- }
- },
- }
- }
- ]
- }
+ "method": "tools/list",
+ "id": 1
}
-```
-
-> [!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
+// Tool invocation
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"
- }
- ]
- }
+ "name": "control_led",
+ "arguments": {"State": "on"}
+ },
+ "id": 2
}
```
-### MCP Protocol Flow
+## Documentation
-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
+| Topic | Description |
+|-------|-------------|
+| [Controllers and Routing](./doc/controllers-routing.md) | Learn about route attributes, method decorations, and URL parameters |
+| [Authentication](./doc/authentication.md) | Configure Basic Auth, API Key, and custom authentication |
+| [HTTPS and Certificates](./doc/https-certificates.md) | Set up SSL/TLS encryption with certificates |
+| [File System Support](./doc/file-system.md) | Serve static files from storage devices |
+| [Model Context Protocol (MCP)](./doc/model-context-protocol.md) | Complete MCP guide for AI agent integration |
+| [REST API Development](./doc/rest-api.md) | Build RESTful APIs with request/response handling |
+| [Event-Driven Programming](./doc/event-driven.md) | Handle requests through events and status monitoring |
+| [Examples and Samples](./doc/examples.md) | Working examples and code samples |
-### Error Handling
+## Limitations
-The MCP implementation provides robust error handling:
+- No compression support in request/response streams
+- MCP implementation supports server features only (no notifications or SSE)
+- No or single parameter limitation for MCP tools (use complex objects for multiple parameters)
-```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()));
-```
+## Installation
-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.
+Install `nanoFramework.WebServer` for the Web Server without File System support. Install `nanoFramework.WebServer.FileSystem` for file serving, so with devices supporting File System.
+Install `nanoFramework.WebServer.Mcp` for MCP support. It does contains the full `nanoFramework.WebServer` but does not include native file serving. You can add this feature fairly easilly by reusing the code function serving it.
-## Feedback and documentation
+## Contributing
-For documentation, providing feedback, issues and finding out how to contribute please refer to the [Home repo](https://github.com/nanoframework/Home).
+For documentation, feedback, issues and contributions, please refer to the [Home repo](https://github.com/nanoframework/Home).
Join our Discord community [here](https://discord.gg/gCyBu8T).
@@ -836,7 +205,7 @@ The list of contributors to this project can be found at [CONTRIBUTORS](https://
## License
-The **nanoFramework** WebServer library is licensed under the [MIT license](LICENSE.md).
+Licensed under the [MIT license](LICENSE.md).
## Code of Conduct
diff --git a/doc/authentication.md b/doc/authentication.md
new file mode 100644
index 0000000..48b920e
--- /dev/null
+++ b/doc/authentication.md
@@ -0,0 +1,310 @@
+# Authentication
+
+This guide covers the authentication options available in nanoFramework WebServer.
+
+## Overview
+
+The WebServer supports three types of authentication that can be applied to controllers and individual methods:
+
+1. **Basic Authentication** - HTTP Basic Auth with username/password
+2. **API Key Authentication** - Custom header-based authentication
+3. **None** - No authentication required
+
+Authentication can be configured at both the class level (applies to all methods) and method level (overrides class-level settings).
+
+## Basic Authentication
+
+### Default Credentials
+
+```csharp
+[Authentication("Basic")]
+public class SecureController
+{
+ [Route("secure/data")]
+ public void GetSecureData(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Secure data");
+ }
+}
+
+// Server setup with default credentials
+using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(SecureController) }))
+{
+ server.Credential = new NetworkCredential("admin", "password");
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+### Custom Credentials
+
+```csharp
+public class UserController
+{
+ [Route("admin")]
+ [Authentication("Basic:admin secretpassword")]
+ public void AdminPanel(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Admin panel");
+ }
+
+ [Route("user")]
+ [Authentication("Basic:user userpass")]
+ public void UserPanel(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "User panel");
+ }
+}
+```
+
+**Note**: The username cannot contain spaces. Use the format: `"Basic:username password"`
+
+## API Key Authentication
+
+### Default API Key
+
+```csharp
+[Authentication("ApiKey")]
+public class ApiController
+{
+ [Route("api/data")]
+ public void GetData(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "API data");
+ }
+}
+
+// Server setup with default API key
+using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ApiController) }))
+{
+ server.ApiKey = "MySecretApiKey123";
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+### Custom API Key
+
+```csharp
+public class ServiceController
+{
+ [Route("service/premium")]
+ [Authentication("ApiKey:premium-key-789")]
+ public void PremiumService(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Premium service");
+ }
+
+ [Route("service/basic")]
+ [Authentication("ApiKey:basic-key-456")]
+ public void BasicService(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Basic service");
+ }
+}
+```
+
+### API Key Usage
+
+Clients must include the API key in the request headers:
+
+```http
+GET /api/data HTTP/1.1
+Host: 192.168.1.100
+ApiKey: MySecretApiKey123
+```
+
+**Note**: The header name `ApiKey` is case-sensitive.
+
+## No Authentication
+
+```csharp
+[Authentication("None")]
+public class PublicController
+{
+ [Route("public/info")]
+ public void GetPublicInfo(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Public information");
+ }
+}
+```
+
+## Mixed Authentication Example
+
+```csharp
+[Authentication("Basic")] // Default for all methods in this class
+public class MixedController
+{
+ [Route("secure/basic")]
+ public void BasicAuth(WebServerEventArgs e)
+ {
+ // Uses class-level Basic authentication
+ WebServer.OutPutStream(e.Context.Response, "Basic auth data");
+ }
+
+ [Route("secure/api")]
+ [Authentication("ApiKey:special-key-123")] // Override with API key
+ public void ApiKeyAuth(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "API key data");
+ }
+
+ [Route("secure/custom")]
+ [Authentication("Basic:customuser custompass")] // Override with custom basic auth
+ public void CustomAuth(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Custom auth data");
+ }
+
+ [Route("public")]
+ [Authentication("None")] // Override to allow public access
+ public void PublicAccess(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Public data");
+ }
+}
+```
+
+## Multiple Authentication for Same Route
+
+The WebServer supports multiple authentication methods for the same route by creating separate methods:
+
+```csharp
+public class MultiAuthController
+{
+ [Route("data")]
+ [Authentication("Basic")]
+ public void DataBasicAuth(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Data via Basic auth");
+ }
+
+ [Route("data")]
+ [Authentication("ApiKey:key1")]
+ public void DataApiKey1(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Data via API key 1");
+ }
+
+ [Route("data")]
+ [Authentication("ApiKey:key2")]
+ public void DataApiKey2(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Data via API key 2");
+ }
+
+ [Route("data")]
+ public void DataPublic(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Public data");
+ }
+}
+```
+
+## Authentication Flow
+
+The server selects the route for a request using this logic:
+
+1. **No matching methods**: Returns 404 (Not Found)
+2. **Authentication provided in request headers**:
+ - Finds methods requiring authentication
+ - If credentials match: Calls the matching method
+ - If credentials don't match: Returns 401 (Unauthorized)
+3. **No authentication provided**:
+ - If a public method exists (no auth required): Calls that method
+ - If only auth-required methods exist: Returns 401 (Unauthorized)
+ - For Basic auth methods: Includes `WWW-Authenticate` header
+
+## Server Configuration
+
+```csharp
+using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(MyController) }))
+{
+ // Set default credentials for Basic authentication
+ server.Credential = new NetworkCredential("defaultuser", "defaultpass");
+
+ // Set default API key
+ server.ApiKey = "DefaultApiKey123";
+
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+## Security Best Practices
+
+1. **Use HTTPS**: Always use HTTPS in production to protect credentials
+2. **Strong Credentials**: Use strong passwords and API keys
+3. **Rotate Keys**: Regularly rotate API keys and passwords
+4. **Principle of Least Privilege**: Grant minimal necessary access
+5. **Secure Storage**: Avoid hardcoding credentials in source code
+
+## HTTPS with Authentication
+
+```csharp
+// Load certificate
+X509Certificate2 cert = new X509Certificate2(certBytes, privateKeyBytes, "password");
+
+using (WebServer server = new WebServer(443, HttpProtocol.Https, new Type[] { typeof(SecureController) }))
+{
+ server.HttpsCert = cert;
+ server.SslProtocols = SslProtocols.Tls12;
+ server.Credential = new NetworkCredential("admin", "securepassword");
+ server.ApiKey = "SecureApiKey456";
+
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+## Testing Authentication
+
+### Basic Auth with curl
+
+```bash
+# With default credentials
+curl -u admin:password http://192.168.1.100/secure/data
+
+# With custom credentials
+curl -u customuser:custompass http://192.168.1.100/secure/custom
+```
+
+### API Key with curl
+
+```bash
+# With API key
+curl -H "ApiKey: MySecretApiKey123" http://192.168.1.100/api/data
+
+# With custom API key
+curl -H "ApiKey: special-key-123" http://192.168.1.100/secure/api
+```
+
+## Error Responses
+
+### 401 Unauthorized (Basic Auth)
+
+```http
+HTTP/1.1 401 Unauthorized
+WWW-Authenticate: Basic realm="nanoFramework"
+Content-Length: 0
+```
+
+### 401 Unauthorized (API Key)
+
+```http
+HTTP/1.1 401 Unauthorized
+Content-Length: 0
+```
+
+### 500 Internal Server Error (Multiple Methods)
+
+When multiple methods match the same route with conflicting authentication, the server returns 500 with details about the conflicting methods.
+
+## Troubleshooting
+
+1. **Always 401**: Check if method requires authentication and credentials are provided
+2. **Wrong credentials**: Verify username/password or API key matches configuration
+3. **Case sensitivity**: API key header name is case-sensitive (`ApiKey`)
+4. **Multiple methods**: Ensure no conflicting methods for the same route/auth combination
+5. **Default vs custom**: Remember that method-level attributes override class-level ones
diff --git a/doc/controllers-routing.md b/doc/controllers-routing.md
new file mode 100644
index 0000000..a85f905
--- /dev/null
+++ b/doc/controllers-routing.md
@@ -0,0 +1,451 @@
+# Controllers and Routing
+
+This guide covers how to use controller-based routing in nanoFramework WebServer.
+
+## Overview
+
+Controllers provide a clean way to organize your web endpoints using attributes and method decorations. Instead of handling all requests in a single event handler, you can create multiple controller classes with decorated methods that handle specific routes.
+
+## Basic Controller Setup
+
+This is a basic example with a controller names `MyController`. You can have as many controllers as you want.
+
+```csharp
+using (WebServer server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(MyController) }))
+{
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+## Route Attributes
+
+Single and multi route is supported.
+
+### Single Route
+
+```csharp
+public class TestController
+{
+ [Route("test")]
+ [Method("GET")]
+ public void GetTest(WebServerEventArgs e)
+ {
+ WebServer.OutPutStream(e.Context.Response, "Test endpoint");
+ }
+}
+```
+
+### Multiple Routes
+
+```csharp
+public class TestController
+{
+ [Route("test"), Route("Test2"), Route("tEst42"), Route("TEST")]
+ [CaseSensitive]
+ [Method("GET")]
+ public void MultipleRoutes(WebServerEventArgs e)
+ {
+ string route = e.Context.Request.RawUrl.TrimStart('/').Split('/')[0];
+ WebServer.OutPutStream(e.Context.Response, $"Route: {route}");
+ }
+}
+```
+
+### Case Sensitivity
+
+By default, routes are **not case sensitive** and the attribute **must** be lowercase. Use `[CaseSensitive]` to have them case sensitive.
+
+```csharp
+public class TestController
+{
+ [Route("test")] // Will match: test, TEST, Test, TeSt, etc.
+ public void CaseInsensitive(WebServerEventArgs e)
+ {
+ // Implementation
+ }
+
+ [Route("Test")] // Case sensitive - matches only "Test"
+ [CaseSensitive]
+ public void CaseSensitive(WebServerEventArgs e)
+ {
+ // Implementation
+ }
+}
+```
+
+## HTTP Methods
+
+This section describes the different methods and how to use them.
+
+### Specific Methods
+
+```csharp
+public class ApiController
+{
+ [Route("api/data")]
+ [Method("GET")]
+ public void GetData(WebServerEventArgs e)
+ {
+ // Handle GET requests
+ }
+
+ [Route("api/data")]
+ [Method("POST")]
+ public void PostData(WebServerEventArgs e)
+ {
+ // Handle POST requests
+ }
+
+ [Route("api/data")]
+ [Method("PUT")]
+ public void PutData(WebServerEventArgs e)
+ {
+ // Handle PUT requests
+ }
+
+ [Route("api/data")]
+ [Method("DELETE")]
+ public void DeleteData(WebServerEventArgs e)
+ {
+ // Handle DELETE requests
+ }
+}
+```
+
+### Any Method
+
+```csharp
+public class TestController
+{
+ [Route("api/any")]
+ public void HandleAnyMethod(WebServerEventArgs e)
+ {
+ // Will handle GET, POST, PUT, DELETE, etc.
+ string method = e.Context.Request.HttpMethod;
+ WebServer.OutPutStream(e.Context.Response, $"Method: {method}");
+ }
+}
+```
+
+## URL Parameters
+
+URL can contains parameters. A specific function `DecodeParam` will allow you to decode them. For path parameters, the pattern is provided below.
+
+### Query Parameters
+
+```csharp
+[Route("api/search")]
+public void Search(WebServerEventArgs e)
+{
+ var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);
+ foreach (var param in parameters)
+ {
+ Debug.WriteLine($"{param.Name}: {param.Value}");
+ }
+}
+```
+
+Example URL: `/api/search?q=nanoframework&category=iot&limit=10`
+
+### Path Parameters
+
+#### Traditional Path Parsing
+
+```csharp
+[Route("api/users")]
+public void GetUser(WebServerEventArgs e)
+{
+ string url = e.Context.Request.RawUrl;
+ string[] segments = url.TrimStart('/').Split('/');
+
+ if (segments.Length > 2)
+ {
+ string userId = segments[2]; // /api/users/123
+ WebServer.OutPutStream(e.Context.Response, $"User ID: {userId}");
+ }
+}
+```
+
+#### Parameterized Routes
+
+nanoFramework WebServer supports parameterized routes with named placeholders using curly braces `{}`. The `GetRouteParameter` method allows you to extract parameter values by name:
+
+```csharp
+[Route("api/users/{id}")]
+public void GetUserById(WebServerEventArgs e)
+{
+ string userId = e.GetRouteParameter("id");
+ WebServer.OutPutStream(e.Context.Response, $"User ID: {userId}");
+}
+
+[Route("api/users/{userId}/sensors/{sensorId}")]
+public void GetUserSensor(WebServerEventArgs e)
+{
+ string userId = e.GetRouteParameter("userId");
+ string sensorId = e.GetRouteParameter("sensorId");
+
+ WebServer.OutPutStream(e.Context.Response,
+ $"User: {userId}, Sensor: {sensorId}");
+}
+
+[Route("api/devices/{deviceId}/measurements/{type}")]
+public void GetDeviceMeasurement(WebServerEventArgs e)
+{
+ string deviceId = e.GetRouteParameter("deviceId");
+ string measurementType = e.GetRouteParameter("type");
+
+ // Example: /api/devices/esp32-001/measurements/temperature
+ WebServer.OutPutStream(e.Context.Response,
+ $"Device {deviceId} - {measurementType} data");
+}
+```
+
+Example URLs that would match these routes:
+- `/api/users/123` → `id = "123"`
+- `/api/users/abc123/sensors/temp01` → `userId = "abc123"`, `sensorId = "temp01"`
+- `/api/devices/esp32-001/measurements/temperature` → `deviceId = "esp32-001"`, `type = "temperature"`
+
+## REST API Example
+
+A very [detailed REST API sample](./rest-api.md) walk through is available as well.
+
+The following example shows the key principles for a REST API using GET, POST and DELETE methods, including both traditional and parameterized routes.
+
+```csharp
+public class PersonController
+{
+ private static ArrayList persons = new ArrayList();
+
+ [Route("api/persons")]
+ [Method("GET")]
+ public void GetPersons(WebServerEventArgs e)
+ {
+ string json = JsonConvert.SerializeObject(persons);
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, json);
+ }
+
+ [Route("api/persons/{id}")]
+ [Method("GET")]
+ public void GetPersonById(WebServerEventArgs e)
+ {
+ string personId = e.GetRouteParameter("id");
+
+ // Find person by ID logic here
+ var person = FindPersonById(personId);
+ if (person != null)
+ {
+ string json = JsonConvert.SerializeObject(person);
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, json);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+
+ [Route("api/persons")]
+ [Method("POST")]
+ public void CreatePerson(WebServerEventArgs e)
+ {
+ if (e.Context.Request.ContentLength64 > 0)
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = Encoding.UTF8.GetString(body, 0, body.Length);
+ var person = JsonConvert.DeserializeObject(json, typeof(Person));
+
+ persons.Add(person);
+
+ e.Context.Response.StatusCode = 201;
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.Created);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ }
+ }
+
+ [Route("api/persons/{id}")]
+ [Method("DELETE")]
+ public void DeletePersonById(WebServerEventArgs e)
+ {
+ string personId = e.GetRouteParameter("id");
+
+ if (personId != null)
+ {
+ // Remove person by ID logic here
+ bool removed = RemovePersonById(personId);
+ if (removed)
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ }
+ }
+
+ // Traditional query parameter approach for compatibility
+ [Route("api/persons")]
+ [Method("DELETE")]
+ public void DeletePerson(WebServerEventArgs e)
+ {
+ var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);
+ string id = null;
+
+ foreach (var param in parameters)
+ {
+ if (param.Name.ToLower() == "id")
+ {
+ id = param.Value;
+ break;
+ }
+ }
+
+ if (id != null)
+ {
+ // Remove person logic here
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ }
+ }
+}
+```
+
+## Best Practices
+
+1. **Organization**: Group related endpoints in the same controller
+2. **Naming**: Use descriptive method names that indicate their purpose
+3. **HTTP Status Codes**: Return appropriate status codes for different scenarios
+4. **Content Types**: Set correct content types for responses
+5. **Error Handling**: Implement proper error handling and validation
+6. **URL Structure**: Use consistent URL patterns (e.g., `/api/resource` for collections)
+7. **Parameterized Routes**: Prefer parameterized routes (`/api/users/{id}`) over query parameters for resource identification as they provide cleaner URLs and better RESTful design
+8. **Parameter Validation**: Always validate route parameters before using them in your logic
+
+## Route Conflicts
+
+nanoFramework WebServer will always try to best handle the route. That said, route conflicts can exist.
+
+If multiple methods match the same route, the server will return an Internal Server Error (500) with details about the conflicting methods. This is considered a programming error that should be fixed during development.
+
+```csharp
+// This will cause a conflict - avoid this!
+public class ConflictController
+{
+ [Route("test")]
+ public void Method1(WebServerEventArgs e) { }
+
+ [Route("test")]
+ public void Method2(WebServerEventArgs e) { } // Conflict!
+}
+```
+
+## Request Data Access
+
+When handling HTTP requests in your controllers, you often need to access data sent by the client. The nanoFramework WebServer provides several ways to extract and process incoming request data through the `WebServerEventArgs` parameter.
+
+The request data can come in various forms:
+
+- **HTTP Headers**: Metadata about the request (content type, authorization, user agent, etc.)
+- **Request Body**: The main payload data sent with POST, PUT, and PATCH requests
+- **URL Parameters**: Query string parameters and path segments
+- **Form Data**: HTML form submissions including file uploads
+
+This section covers how to access headers and request body data in your controller methods.
+
+### Headers
+
+HTTP headers contain important metadata about the request. Common headers include `Content-Type`, `Authorization`, `User-Agent`, and custom headers. You can access all headers through the `Headers` collection:
+
+```csharp
+[Route("api/info")]
+public void GetRequestInfo(WebServerEventArgs e)
+{
+ foreach (string headerName in e.Context.Request.Headers.AllKeys)
+ {
+ string[] values = e.Context.Request.Headers.GetValues(headerName);
+ Debug.WriteLine($"Header: {headerName} = {string.Join(", ", values)}");
+ }
+}
+
+### Request Body
+
+```csharp
+[Route("api/upload")]
+[Method("POST")]
+public void HandleUpload(WebServerEventArgs e)
+{
+ if (e.Context.Request.ContentLength64 > 0)
+ {
+ var contentTypes = e.Context.Request.Headers?.GetValues("Content-Type");
+ var isMultipartForm = contentTypes != null && contentTypes.Length > 0 &&
+ contentTypes[0].StartsWith("multipart/form-data;");
+
+ if (isMultipartForm)
+ {
+ var form = e.Context.Request.ReadForm();
+ Debug.WriteLine($"Form has {form.Parameters.Length} parameters and {form.Files.Length} files");
+ }
+ else
+ {
+ var body = e.Context.Request.ReadBody();
+ string content = Encoding.UTF8.GetString(body, 0, body.Length);
+ Debug.WriteLine($"Body content: {content}");
+ }
+ }
+}
+```
+
+## Response Helpers
+
+This section contains how to handle JSON and HTML responses.
+
+### JSON Response
+
+```csharp
+[Route("api/data")]
+public void GetData(WebServerEventArgs e)
+{
+ var data = new { message = "Hello", timestamp = DateTime.UtcNow };
+ string json = JsonConvert.SerializeObject(data);
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, json);
+}
+```
+
+### HTML Response
+
+```csharp
+[Route("page")]
+public void GetPage(WebServerEventArgs e)
+{
+ string html = "Hello nanoFramework!
";
+ e.Context.Response.ContentType = "text/html";
+ WebServer.OutPutStream(e.Context.Response, html);
+}
+```
+
+### Custom Status Codes
+
+Bonus point if you read up to here, you can also create your custom status code!
+
+```csharp
+[Route("api/status")]
+public void CustomStatus(WebServerEventArgs e)
+{
+ e.Context.Response.StatusCode = 418; // I'm a teapot
+ WebServer.OutPutStream(e.Context.Response, "I'm a teapot!");
+}
+```
diff --git a/doc/event-driven.md b/doc/event-driven.md
new file mode 100644
index 0000000..792d9d4
--- /dev/null
+++ b/doc/event-driven.md
@@ -0,0 +1,777 @@
+# Event-Driven Programming
+
+The nanoFramework WebServer provides a powerful event-driven architecture that allows developers to handle HTTP requests dynamically and monitor server status changes. This approach offers flexibility for scenarios where attribute-based routing isn't sufficient or when you need fine-grained control over request processing.
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [CommandReceived Event](#commandreceived-event)
+3. [WebServerStatusChanged Event](#webserverstatuschanged-event)
+4. [WebServerEventArgs](#webservereventargs)
+5. [Basic Event Handling Examples](#basic-event-handling-examples)
+6. [Advanced Scenarios](#advanced-scenarios)
+7. [Event Handling Best Practices](#event-handling-best-practices)
+8. [Error Handling](#error-handling)
+9. [Performance Considerations](#performance-considerations)
+
+## Overview
+
+The nanoFramework WebServer supports two primary events:
+
+- **CommandReceived**: Triggered when an HTTP request is received that doesn't match any registered controller routes
+- **WebServerStatusChanged**: Triggered when the server status changes (starting, running, stopped)
+
+Event-driven programming is particularly useful for:
+
+- Dynamic request handling without predefined routes
+- Custom authentication and authorization logic
+- Request logging and monitoring
+- Server lifecycle management
+- Fallback handling for unmatched routes
+
+## CommandReceived Event
+
+The `CommandReceived` event is the primary mechanism for handling HTTP requests in an event-driven manner.
+
+### Event Signature
+
+```csharp
+public delegate void GetRequestHandler(object obj, WebServerEventArgs e);
+public event GetRequestHandler CommandReceived;
+```
+
+### Basic Usage
+
+```csharp
+using System;
+using System.Diagnostics;
+using System.Net;
+using nanoFramework.WebServer;
+
+// Create WebServer instance
+var server = new WebServer(80, HttpProtocol.Http);
+
+// Subscribe to the event
+server.CommandReceived += ServerCommandReceived;
+
+// Start the server
+server.Start();
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+ var method = e.Context.Request.HttpMethod;
+
+ Debug.WriteLine($"Command received: {url}, Method: {method}");
+
+ if (url.ToLower() == "/hello")
+ {
+ WebServer.OutPutStream(e.Context.Response, "Hello from nanoFramework!");
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+### Parameter Handling
+
+Extract and process URL parameters in event handlers:
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.ToLower().IndexOf("/param.htm") == 0)
+ {
+ // Extract parameters from URL
+ var parameters = WebServer.DecodeParam(url);
+
+ string response = "Parameters";
+ response += "URL Parameters:
";
+
+ if (parameters != null)
+ {
+ foreach (var param in parameters)
+ {
+ response += $"Parameter: {param.Name} = {param.Value}
";
+ }
+ }
+
+ response += "";
+ WebServer.OutPutStream(e.Context.Response, response);
+ }
+}
+```
+
+### File Serving Example
+
+> [!IMPORTANT] You need support for File System to use the file `nanoFramework.WebSer.FileSystem` nuget.
+
+The following example shows the basic to download and create a file. In this example, they'll both be writen and read from then internal storage.
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.IndexOf("/download/") == 0)
+ {
+ string fileName = url.Substring(10); // Remove "/download/"
+ string filePath = $"I:\\{fileName}";
+
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+ else if (url.ToLower() == "/createfile")
+ {
+ // Create a test file
+ File.WriteAllText("I:\\test.txt", "This is a dynamically created file");
+ WebServer.OutPutStream(e.Context.Response, "File created successfully");
+ }
+}
+```
+
+## WebServerStatusChanged Event
+
+Monitor server status changes to implement robust server management.
+
+### Event Signature
+
+```csharp
+public delegate void WebServerStatusHandler(object obj, WebServerStatusEventArgs e);
+public event WebServerStatusHandler WebServerStatusChanged;
+```
+
+### Status Values
+
+The server can be in one of the following states:
+
+```csharp
+public enum WebServerStatus
+{
+ Stopped,
+ Running
+}
+```
+
+### Basic Status Monitoring
+
+```csharp
+var server = new WebServer(80, HttpProtocol.Http);
+
+// Subscribe to status changes
+server.WebServerStatusChanged += OnWebServerStatusChanged;
+
+server.Start();
+
+private static void OnWebServerStatusChanged(object obj, WebServerStatusEventArgs e)
+{
+ Debug.WriteLine($"Server status changed to: {e.Status}");
+
+ if (e.Status == WebServerStatus.Running)
+ {
+ Debug.WriteLine("Server is now accepting requests");
+ // Initialize additional services
+ }
+ else if (e.Status == WebServerStatus.Stopped)
+ {
+ Debug.WriteLine("Server has stopped");
+ // Cleanup or restart logic
+ }
+}
+```
+
+### Server Recovery Pattern
+
+```csharp
+private static void OnWebServerStatusChanged(object obj, WebServerStatusEventArgs e)
+{
+ if (e.Status == WebServerStatus.Stopped)
+ {
+ Debug.WriteLine("Server stopped unexpectedly. Attempting restart...");
+
+ // Wait a moment before restart
+ Thread.Sleep(5000);
+
+ try
+ {
+ var server = (WebServer)obj;
+ if (server.Start())
+ {
+ Debug.WriteLine("Server successfully restarted");
+ }
+ else
+ {
+ Debug.WriteLine("Failed to restart server");
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error restarting server: {ex.Message}");
+ // You may want to reboot your board for example here.
+ }
+ }
+}
+```
+
+## WebServerEventArgs
+
+The `WebServerEventArgs` class provides access to the HTTP context and all request/response information.
+
+### Properties
+
+```csharp
+public class WebServerEventArgs
+{
+ public HttpListenerContext Context { get; protected set; }
+}
+```
+
+### Accessing Request Information
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ // Request properties
+ string url = request.RawUrl;
+ string method = request.HttpMethod;
+ string contentType = request.ContentType;
+ var headers = request.Headers;
+ var inputStream = request.InputStream;
+
+ // Process request data
+ if (method == "POST" && request.InputStream.Length > 0)
+ {
+ byte[] buffer = new byte[request.InputStream.Length];
+ request.InputStream.Read(buffer, 0, buffer.Length);
+ string postData = System.Text.Encoding.UTF8.GetString(buffer, 0, buffer.Length);
+
+ Debug.WriteLine($"POST data: {postData}");
+ }
+
+ // Send response
+ WebServer.OutPutStream(response, "Request processed successfully");
+}
+```
+
+## Basic Event Handling Examples
+
+This section will present various event handeling patterns.
+
+### Simple Text Response
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ switch (url.ToLower())
+ {
+ case "/":
+ WebServer.OutPutStream(e.Context.Response, "Welcome to nanoFramework WebServer!");
+ break;
+
+ case "/time":
+ WebServer.OutPutStream(e.Context.Response, $"Current time: {DateTime.UtcNow}");
+ break;
+
+ case "/info":
+ var info = $"Server running on nanoFramework\nUptime: {Environment.TickCount}ms";
+ WebServer.OutPutStream(e.Context.Response, info);
+ break;
+
+ default:
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ break;
+ }
+}
+```
+
+### HTML Response with Dynamic Content
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.ToLower() == "/dashboard")
+ {
+ // Note: in the real life, you'll remove all the carrier returns and spaces to save spae and make it as efficient
+ // This is nicely stringify for easy reading.
+ string html = $@"
+
+
+ nanoFramework Dashboard
+
+
+
+ System Dashboard
+ Current Time: {DateTime.UtcNow}
+ Uptime: {Environment.TickCount} ms
+ Free Memory: {System.GC.GetTotalMemory(false)} bytes
+ Home | Info
+
+ ";
+
+ WebServer.OutPutStream(e.Context.Response, html);
+ }
+}
+```
+
+### JSON API Response
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.ToLower().IndexOf("/api/") == 0)
+ {
+ e.Context.Response.ContentType = "application/json";
+
+ if (url.ToLower() == "/api/status")
+ {
+ string json = $@"{{
+ ""status"": ""running"",
+ ""timestamp"": ""{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}"",
+ ""uptime"": {Environment.TickCount},
+ ""memory"": {System.GC.GetTotalMemory(false)}
+ }}";
+
+ WebServer.OutPutStream(e.Context.Response, json);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+}
+```
+
+## Advanced Scenarios
+
+This section presents advance scenarios.
+
+### Request Logging and Analytics
+
+This is a simple logging scenario. Note that [nanoFramework also provides a logging library](https://github.com/nanoframework/nanoFramework.Logging).
+
+```csharp
+private static readonly ArrayList RequestLog = new ArrayList();
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+
+ // Log request details
+ var logEntry = new
+ {
+ Timestamp = DateTime.UtcNow,
+ Method = request.HttpMethod,
+ Url = request.RawUrl,
+ UserAgent = request.Headers["User-Agent"],
+ RemoteEndPoint = request.RemoteEndPoint?.ToString()
+ };
+
+ RequestLog.Add(logEntry);
+ Debug.WriteLine($"Request logged: {request.HttpMethod} {request.RawUrl}");
+
+ // Limit log size
+ if (RequestLog.Count > 100)
+ {
+ RequestLog.RemoveAt(0);
+ }
+
+ // Handle request normally
+ HandleRequest(e);
+}
+
+private static void HandleRequest(WebServerEventArgs e)
+{
+ // Your normal request handling logic
+ var url = e.Context.Request.RawUrl;
+
+ if (url == "/logs")
+ {
+ // Return request logs
+ string response = "Recent Requests:\n";
+ foreach (var entry in RequestLog)
+ {
+ response += $"{entry}\n";
+ }
+ WebServer.OutPutStream(e.Context.Response, response);
+ }
+ else
+ {
+ // Handle other requests
+ WebServer.OutPutStream(e.Context.Response, "Request processed");
+ }
+}
+```
+
+### Custom Authentication
+
+This example shows you how you can manage your own authentication mechanism.
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var url = request.RawUrl;
+
+ // Check if route requires authentication
+ if (RequiresAuth(url))
+ {
+ if (!IsAuthenticated(request))
+ {
+ e.Context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Secure Area\"");
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.Unauthorized);
+ return;
+ }
+ }
+
+ // Process authenticated request
+ HandleAuthenticatedRequest(e);
+}
+
+private static bool RequiresAuth(string url)
+{
+ return url.StartsWith("/admin/") || url.StartsWith("/secure/");
+}
+
+private static bool IsAuthenticated(HttpListenerRequest request)
+{
+ var credentials = request.Credentials;
+ if (credentials == null) return false;
+
+ // Check credentials against your authentication system
+ return credentials.UserName == "admin" && credentials.Password == "password";
+}
+```
+
+### Content Type Handling
+
+Here is a simple pattern that will allow you to handle different types of content published.
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ // Handle different content types
+ switch (request.ContentType?.ToLower())
+ {
+ case "application/json":
+ HandleJsonRequest(e);
+ break;
+
+ case "application/x-www-form-urlencoded":
+ HandleFormRequest(e);
+ break;
+
+ case "multipart/form-data":
+ HandleMultipartRequest(e);
+ break;
+
+ default:
+ HandleDefaultRequest(e);
+ break;
+ }
+}
+
+private static void HandleJsonRequest(WebServerEventArgs e)
+{
+ // Read JSON payload
+ var buffer = new byte[e.Context.Request.InputStream.Length];
+ e.Context.Request.InputStream.Read(buffer, 0, buffer.Length);
+ string jsonData = System.Text.Encoding.UTF8.GetString(buffer, 0, buffer.Length);
+
+ Debug.WriteLine($"Received JSON: {jsonData}");
+
+ // Process JSON and respond
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, "{\"status\":\"success\"}");
+}
+```
+
+## Event Handling Best Practices
+
+This section provides good practices and patterns.
+
+### 1. Always Handle Exceptions
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ try
+ {
+ HandleRequest(e);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error handling request: {ex.Message}");
+
+ try
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.InternalServerError);
+ }
+ catch
+ {
+ // Context might be disposed, ignore
+ }
+ }
+}
+```
+
+### 2. Use Asynchronous Processing for Long Operations
+
+While nanoFramework does not have a await/async yet, you can use threads. And for long running tasks, use `Threads` to run your long process, and provide then another endpoint to check if your process is finished or not.
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url == "/longprocess")
+ {
+ // Start long operation in background thread
+ new Thread(() =>
+ {
+ try
+ {
+ ProcessLongRunningTask(e);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Background task error: {ex.Message}");
+ }
+ }).Start();
+
+ // Return immediate response
+ WebServer.OutPutStream(e.Context.Response, "Processing started");
+ }
+ // Here, you would implement another routing for returning the status of your long process.
+}
+```
+
+### 3. Validate Input Data
+
+One of the key elements is to never trust what's send to you and always validate everything. If you are using controllers, this is done for you. If you can't use controlles, then, always check the inputs.
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+
+ // Validate HTTP method
+ if (request.HttpMethod != "GET" && request.HttpMethod != "POST")
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.MethodNotAllowed);
+ return;
+ }
+
+ // Validate URL format
+ if (string.IsNullOrEmpty(request.RawUrl))
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ return;
+ }
+
+ // Continue with processing
+ HandleValidatedRequest(e);
+}
+```
+
+### 4. Implement Proper Resource Cleanup
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ FileStream fileStream = null;
+
+ try
+ {
+ var url = e.Context.Request.RawUrl;
+
+ if (url.StartsWith("/file/"))
+ {
+ string fileName = url.Substring(6);
+ fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
+
+ // Process file
+ WebServer.SendFileOverHTTP(e.Context.Response, fileName);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error: {ex.Message}");
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.InternalServerError);
+ }
+ finally
+ {
+ fileStream?.Dispose();
+ }
+}
+```
+
+## Error Handling
+
+This section provides details on error handeling for specific cases typically out of memory and IO exceptions. While this appear in this overall event driven section, this can also apply to controllers.
+
+### Graceful Error Recovery
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ try
+ {
+ ProcessRequest(e);
+ }
+ catch (OutOfMemoryException)
+ {
+ // Force garbage collection
+ System.GC.Collect();
+
+ response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ WebServer.OutPutStream(response, "Service temporarily unavailable - low memory");
+ }
+ catch (System.IO.IOException ioEx)
+ {
+ Debug.WriteLine($"IO Error: {ioEx.Message}");
+ WebServer.OutputHttpCode(response, HttpStatusCode.InternalServerError);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Unexpected error: {ex.Message}");
+ WebServer.OutputHttpCode(response, HttpStatusCode.InternalServerError);
+ }
+}
+```
+
+### Request Timeout Handling
+
+Here is an handy pattern with a simple timer to manage timeout. You can acheive something similar with cancellation token as well.
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var timeoutTimer = new Timer(HandleTimeout, e.Context, 30000, Timeout.Infinite);
+
+ try
+ {
+ ProcessRequest(e);
+ timeoutTimer.Dispose();
+ }
+ catch
+ {
+ timeoutTimer.Dispose();
+ throw;
+ }
+}
+
+private static void HandleTimeout(object state)
+{
+ var context = (HttpListenerContext)state;
+
+ try
+ {
+ WebServer.OutputHttpCode(context.Response, HttpStatusCode.RequestTimeout);
+ }
+ catch
+ {
+ // Context might be disposed
+ }
+}
+```
+
+## Performance Considerations
+
+While those advices are in this event driven section, they will also apply in controllers.
+
+### 1. Minimize Allocations in Event Handlers
+
+```csharp
+// Reuse string builders and buffers
+private static readonly StringBuilder ResponseBuilder = new StringBuilder();
+private static readonly byte[] Buffer = new byte[1024];
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ ResponseBuilder.Clear();
+ ResponseBuilder.Append("Response data: ");
+ ResponseBuilder.Append(DateTime.UtcNow);
+
+ WebServer.OutPutStream(e.Context.Response, ResponseBuilder.ToString());
+}
+```
+
+### 2. Cache Frequently Used Data
+
+```csharp
+private static readonly Hashtable ResponseCache = new Hashtable();
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ // Check cache first
+ if (ResponseCache.Contains(url))
+ {
+ string cachedResponse = (string)ResponseCache[url];
+ WebServer.OutPutStream(e.Context.Response, cachedResponse);
+ return;
+ }
+
+ // Generate response
+ string response = GenerateResponse(url);
+
+ // Cache response (with size limit)
+ if (ResponseCache.Count < 50)
+ {
+ ResponseCache[url] = response;
+ }
+
+ WebServer.OutPutStream(e.Context.Response, response);
+}
+```
+
+### 3. Use Efficient String Operations
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ // Use IndexOf instead of StartsWith for better performance on nanoFramework
+ if (url.IndexOf("/api/") == 0)
+ {
+ HandleApiRequest(e);
+ }
+ else if (url.IndexOf("/static/") == 0)
+ {
+ HandleStaticContent(e);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+The event-driven approach provides maximum flexibility for handling HTTP requests in nanoFramework applications. By combining the `CommandReceived` and `WebServerStatusChanged` events with proper error handling and performance considerations, you can build robust and responsive web applications that handle a wide variety of scenarios.
diff --git a/doc/file-system.md b/doc/file-system.md
new file mode 100644
index 0000000..b0731b6
--- /dev/null
+++ b/doc/file-system.md
@@ -0,0 +1,523 @@
+# File System Support
+
+The nanoFramework WebServer provides comprehensive support for serving static files from various storage devices including SD cards, USB storage, and internal flash storage.
+
+## Overview
+
+File system support is provided through the [`nanoFramework.WebServer.FileSystem`](https://www.nuget.org/packages/nanoFramework.WebServer.FileSystem/) NuGet package, which enables your WebServer to:
+
+- **Serve static files** from any mounted storage device
+- **Automatic MIME type detection** based on file extensions
+- **Efficient file streaming** with chunked transfer for large files
+- **Support multiple storage types** (SD Card, USB, Internal Storage)
+- **Memory-efficient serving** with configurable buffer sizes
+
+## Requirements
+
+- **NuGet Package**: `nanoFramework.WebServer.FileSystem`
+- **Device Capability**: Target device must support `System.IO.FileSystem`
+- **Storage Device**: SD Card, USB storage, or internal flash storage
+
+## Storage Types and Paths
+
+Different storage devices are mounted with specific drive letters:
+
+| Storage Type | Drive Letter | Example Path | Notes |
+|--------------|--------------|--------------|-------|
+| Internal Storage | `I:\` | `I:\webpage.html` | Built-in flash storage |
+| SD Card | `D:\` | `D:\images\logo.png` | Requires SD card mounting |
+| USB Storage | `E:\` | `E:\documents\file.pdf` | USB mass storage devices |
+
+## Basic File Serving
+
+### Event-Driven Approach
+
+```csharp
+using (WebServer server = new WebServer(80, HttpProtocol.Http))
+{
+ server.CommandReceived += ServerCommandReceived;
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ const string DirectoryPath = "I:\\"; // Internal storage
+ var url = e.Context.Request.RawUrl;
+ var fileName = url.Substring(1); // Remove leading '/'
+
+ // Check if file exists and serve it
+ string filePath = DirectoryPath + fileName;
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+### Controller-Based Approach
+
+```csharp
+public class FileController
+{
+ private const string StoragePath = "D:\\"; // SD Card storage
+
+ [Route("files")]
+ [Method("GET")]
+ public void ServeFiles(WebServerEventArgs e)
+ {
+ var url = e.Context.Request.RawUrl;
+ var fileName = url.Substring("/files/".Length);
+
+ string filePath = StoragePath + fileName;
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+}
+```
+
+## Advanced File Serving
+
+### Directory Listing and File Discovery
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ const string DirectoryPath = "I:\\";
+ var url = e.Context.Request.RawUrl;
+
+ // Get list of all files in directory
+ string[] fileList = Directory.GetFiles(DirectoryPath);
+
+ // Remove directory path from file names for comparison
+ for (int i = 0; i < fileList.Length; i++)
+ {
+ fileList[i] = fileList[i].Substring(DirectoryPath.Length);
+ }
+
+ var requestedFile = url.Substring(1); // Remove leading '/'
+
+ // Search for the requested file (case-sensitive)
+ foreach (var file in fileList)
+ {
+ if (file == requestedFile)
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, DirectoryPath + file);
+ return;
+ }
+ }
+
+ // File not found
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+}
+```
+
+### Serving Files from Memory
+
+You can also serve file content directly from memory without physical files:
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url == "/dynamic.txt")
+ {
+ string content = $"Generated at: {DateTime.UtcNow}";
+ byte[] contentBytes = Encoding.UTF8.GetBytes(content);
+
+ WebServer.SendFileOverHTTP(e.Context.Response, "dynamic.txt", contentBytes, "text/plain");
+ }
+ else if (url == "/config.json")
+ {
+ string jsonContent = "{\"server\":\"nanoFramework\",\"version\":\"1.0\"}";
+ byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonContent);
+
+ WebServer.SendFileOverHTTP(e.Context.Response, "config.json", jsonBytes);
+ }
+}
+```
+
+## MIME Type Detection
+
+The WebServer automatically detects MIME types based on file extensions:
+
+| Extension | MIME Type | Description |
+|-----------|-----------|-------------|
+| `.html`, `.htm` | `text/html` | HTML pages |
+| `.txt`, `.cs`, `.csproj` | `text/plain` | Plain text files |
+| `.css` | `text/css` | Stylesheets |
+| `.js` | `application/javascript` | JavaScript files |
+| `.json` | `application/json` | JSON data |
+| `.pdf` | `application/pdf` | PDF documents |
+| `.zip` | `application/zip` | ZIP archives |
+| `.jpg`, `.jpeg` | `image/jpeg` | JPEG images |
+| `.png` | `image/png` | PNG images |
+| `.gif` | `image/gif` | GIF images |
+| `.bmp` | `image/bmp` | Bitmap images |
+| `.ico` | `image/x-icon` | Icon files |
+| `.mp3` | `audio/mpeg` | MP3 audio |
+
+### Custom MIME Types
+
+You can specify custom MIME types when serving files:
+
+```csharp
+// Serve with custom MIME type
+WebServer.SendFileOverHTTP(e.Context.Response, filePath, "application/custom");
+
+// Serve from memory with custom MIME type
+WebServer.SendFileOverHTTP(e.Context.Response, "data.bin", binaryData, "application/octet-stream");
+```
+
+## SD Card Setup
+
+To serve files from an SD Card, you need to mount it first:
+
+```csharp
+// Mount SD card (device-specific implementation)
+// See: https://github.com/nanoframework/Samples/blob/main/samples/System.IO.FileSystem/MountExample/Program.cs
+
+public static void Main()
+{
+ // Mount SD card to D: drive
+ // This is device-specific - check your device documentation
+
+ // Start WebServer after SD card is mounted
+ using (WebServer server = new WebServer(80, HttpProtocol.Http))
+ {
+ server.CommandReceived += ServerCommandReceived;
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+ }
+}
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ const string SdCardPath = "D:\\"; // SD Card mount point
+ var fileName = e.Context.Request.RawUrl.Substring(1);
+
+ string filePath = SdCardPath + fileName;
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+## Performance Considerations
+
+### Buffer Size
+
+The WebServer uses an internal buffer for file streaming. For large files, the content is sent in chunks to manage memory efficiently.
+
+### File Caching
+
+For frequently accessed small files, consider loading them into memory at startup (note that is is consume memory, you should only do this on boards where you have enough memory):
+
+```csharp
+public class CachedFileServer
+{
+ private static readonly Hashtable _fileCache = new Hashtable();
+
+ static CachedFileServer()
+ {
+ // Cache frequently accessed files
+ CacheFile("index.html", "I:\\index.html");
+ CacheFile("style.css", "I:\\style.css");
+ CacheFile("script.js", "I:\\script.js");
+ }
+
+ private static void CacheFile(string name, string path)
+ {
+ if (File.Exists(path))
+ {
+ byte[] content = File.ReadAllBytes(path);
+ _fileCache[name] = content;
+ }
+ }
+
+ public static void ServeFile(WebServerEventArgs e, string fileName)
+ {
+ if (_fileCache.Contains(fileName))
+ {
+ byte[] content = (byte[])_fileCache[fileName];
+ WebServer.SendFileOverHTTP(e.Context.Response, fileName, content);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+}
+```
+
+## Security Considerations
+
+Here are some important security considerations.
+
+### Path Traversal Protection
+
+If you are sticked to a specific directory, you may want to validate file paths to prevent directory traversal attacks:
+
+```csharp
+private static bool IsValidPath(string fileName)
+{
+ // Reject paths with directory traversal attempts
+ if (fileName.Contains("..") || fileName.Contains("\\") || fileName.Contains("/"))
+ {
+ return false;
+ }
+
+ // Only allow alphanumeric characters, dots, and hyphens
+ foreach (char c in fileName)
+ {
+ if (!char.IsLetterOrDigit(c) && c != '.' && c != '-' && c != '_')
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var fileName = e.Context.Request.RawUrl.Substring(1);
+
+ if (!IsValidPath(fileName))
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ return;
+ }
+
+ // Proceed with file serving
+ string filePath = "I:\\yourdirectory\\" + fileName;
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+### File Access Control
+
+Implement authentication (here implemented with basic, [more information on authentication](./authentication.md)) for sensitive files:
+
+```csharp
+[Route("secure")]
+[Authentication("Basic")]
+public class SecureFileController
+{
+ [Route("documents")]
+ [Method("GET")]
+ public void ServeSecureFiles(WebServerEventArgs e)
+ {
+ // Only authenticated users can access these files
+ var fileName = e.Context.Request.QueryString["file"];
+ string filePath = "I:\\secure\\" + fileName;
+
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+}
+```
+
+## Complete Example
+
+Here's a complete file server implementation (you'll have to add the nanoFramework `System.Device.Wifi` nuget):
+
+```csharp
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using nanoFramework.Networking;
+using nanoFramework.WebServer;
+
+public class Program
+{
+ private const string StoragePath = "I:\\";
+
+ public static void Main()
+ {
+ // This connects to the wifi
+ var res = WifiNetworkHelper.ConnectDhcp("YourSsid", "YourPassword", 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()}");
+
+ // Initialize storage and create sample files
+ InitializeStorage();
+
+ using (WebServer server = new WebServer(80, HttpProtocol.Http))
+ {
+ server.CommandReceived += ServerCommandReceived;
+ server.Start();
+
+ Console.WriteLine($"Serving files from: {StoragePath}");
+
+ Thread.Sleep(Timeout.Infinite);
+ }
+ }
+
+ private static void InitializeStorage()
+ {
+ // Create sample files
+ if (!File.Exists(StoragePath + "index.html"))
+ {
+ string html = @"
+
+nanoFramework File Server
+
+ Welcome to nanoFramework File Server
+
+
+";
+ File.WriteAllText(StoragePath + "index.html", html);
+ }
+
+ if (!File.Exists(StoragePath + "sample.txt"))
+ {
+ File.WriteAllText(StoragePath + "sample.txt", "Hello from nanoFramework!");
+ }
+
+ if (!File.Exists(StoragePath + "data.json"))
+ {
+ string json = "{\"message\":\"Hello\",\"timestamp\":\"" + DateTime.UtcNow.ToString() + "\"}";
+ File.WriteAllText(StoragePath + "data.json", json);
+ }
+ }
+
+ private static void ServerCommandReceived(object source, WebServerEventArgs e)
+ {
+ var url = e.Context.Request.RawUrl;
+ var fileName = url == "/" ? "index.html" : url.Substring(1);
+
+ // Validate file name for security
+ if (!IsValidFileName(fileName))
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ return;
+ }
+
+ string filePath = StoragePath + fileName;
+
+ if (File.Exists(filePath))
+ {
+ Console.WriteLine($"Serving file: {fileName}");
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ Console.WriteLine($"File not found: {fileName}");
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+
+ private static bool IsValidFileName(string fileName)
+ {
+ return !string.IsNullOrEmpty(fileName) &&
+ !fileName.Contains("..") &&
+ !fileName.Contains("\\") &&
+ !fileName.StartsWith("/");
+ }
+
+ private static string GetCurrentIPAddress()
+ {
+ NetworkInterface ni = NetworkInterface.GetAllNetworkInterfaces()[0];
+
+ // get first NI ( Wifi on ESP32 )
+ return ni.IPv4Address.ToString();
+ }
+}
+```
+
+## Troubleshooting
+
+This section will give you couple of tips and tricks to help you find potential issues. Few things to keep in mind:
+
+- `Console.WriteLine` will always display the message in the output
+- `Debug.WriteLine` will only display it when debug is enabled
+
+If you are trying to understand what's happening in release mode, use `Console.WriteLine` and connect to the com port at the 921600 speed.
+
+### Common Issues
+
+1. **File Not Found**: Ensure the file path is correct and the file exists
+2. **Permission Denied**: Check file system permissions and device capabilities
+3. **Memory Issues**: Use file streaming for large files instead of loading into memory
+4. **SD Card Not Mounted**: Verify SD card mounting before serving files
+
+### Debug Tips
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var fileName = e.Context.Request.RawUrl.Substring(1);
+ string filePath = StoragePath + fileName;
+
+ Console.WriteLine($"Requested file: {fileName}");
+ Console.WriteLine($"Full path: {filePath}");
+ Console.WriteLine($"File exists: {File.Exists(filePath)}");
+
+ if (File.Exists(filePath))
+ {
+ var fileInfo = new FileInfo(filePath);
+ Console.WriteLine($"File size: {fileInfo.Length} bytes");
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ // List available files for debugging
+ string[] files = Directory.GetFiles(StoragePath);
+ Console.WriteLine("Available files:");
+ foreach (var file in files)
+ {
+ Console.WriteLine($" {file.Substring(StoragePath.Length)}");
+ }
+
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+## Related Resources
+
+- [SD Card Mounting Example](https://github.com/nanoframework/Samples/blob/main/samples/System.IO.FileSystem/MountExample/Program.cs)
+- [WebServer E2E Tests](../tests/WebServerE2ETests/) - Contains file serving examples
+- [Controllers and Routing](./controllers-routing.md) - For controller-based file serving
+- [Authentication](./authentication.md) - For securing file access
+
+The file system support in nanoFramework WebServer provides a robust foundation for serving static content from embedded devices, making it easy to create web interfaces, serve configuration files, or provide downloadable content directly from your IoT devices.
diff --git a/doc/https-certificates.md b/doc/https-certificates.md
new file mode 100644
index 0000000..36e03b8
--- /dev/null
+++ b/doc/https-certificates.md
@@ -0,0 +1,446 @@
+# HTTPS and Certificates
+
+This guide covers how to configure HTTPS/SSL encryption with certificates in nanoFramework WebServer.
+
+## Overview
+
+The WebServer supports HTTPS connections using X.509 certificates for encrypted communication. This is essential for production deployments, especially when using authentication or transmitting sensitive data.
+
+## Certificate Generation
+
+This section will give you an overview of basic certificate generation and how to use those with nanoFramework.
+
+### Using OpenSSL
+
+You can use `openssl` to generate a self-signed certificate for testing. Download on different platforms is [available here](https://github.com/openssl/openssl?tab=readme-ov-file#download).
+
+```bash
+# Generate private key and certificate
+openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
+
+# Create password-protected private key
+openssl rsa -des3 -in server.key -out server-encrypted.key
+# Enter password when prompted (e.g., "1234")
+```
+
+### Certificate Files
+
+You'll get two files:
+- `server.crt` - The certificate (public key)
+- `server-encrypted.key` - The encrypted private key
+
+## Basic HTTPS Setup
+
+With the generated certificates in the previous step:
+
+```csharp
+public static void Main()
+{
+ // Certificate as string constants
+ const string serverCrt = @"-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAKL0UG+mRnNjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+... (rest of certificate)
+-----END CERTIFICATE-----";
+
+ const string serverKey = @"-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,2B9FDBE5B2B6AD34
+
+WgJz8pSS8RQUFNjPsrG/s2pGYCJ5FghVnS5s6H+mDfY0+XqJdBcm0I2WZLy/
+... (rest of encrypted private key)
+-----END RSA PRIVATE KEY-----";
+
+ // Create certificate object
+ X509Certificate2 certificate = new X509Certificate2(
+ Encoding.UTF8.GetBytes(serverCrt),
+ Encoding.UTF8.GetBytes(serverKey),
+ "1234" // Password used to encrypt the private key
+ );
+
+ // Create HTTPS server
+ using (WebServer server = new WebServer(443, HttpProtocol.Https, new Type[] { typeof(MyController) }))
+ {
+ server.HttpsCert = certificate;
+ server.SslProtocols = SslProtocols.Tls12;
+
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+ }
+}
+```
+
+## Complete HTTPS Example
+
+```csharp
+public class SecureController
+{
+ [Route("secure/data")]
+ [Authentication("Basic")]
+ public void GetSecureData(WebServerEventArgs e)
+ {
+ var data = new
+ {
+ message = "Secure data over HTTPS",
+ timestamp = DateTime.UtcNow,
+ encrypted = true
+ };
+
+ string json = JsonConvert.SerializeObject(data);
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, json);
+ }
+}
+
+public static void Main()
+{
+ // Connect to WiFi
+ var connected = WifiNetworkHelper.ConnectDhcp(ssid, password, requiresDateTime: true);
+ if (!connected) return;
+
+ // Load certificate
+ X509Certificate2 cert = LoadCertificate();
+
+ using (WebServer server = new WebServer(443, HttpProtocol.Https, new Type[] { typeof(SecureController) }))
+ {
+ // Configure HTTPS
+ server.HttpsCert = cert;
+ server.SslProtocols = SslProtocols.Tls12;
+
+ // Configure authentication
+ server.Credential = new NetworkCredential("admin", "securepass");
+
+ server.Start();
+ Debug.WriteLine("HTTPS server started on port 443");
+ Thread.Sleep(Timeout.Infinite);
+ }
+}
+
+private static X509Certificate2 LoadCertificate()
+{
+ // Certificate content (replace with your actual certificate)
+ const string certContent = @"-----BEGIN CERTIFICATE-----
+... your certificate content here ...
+-----END CERTIFICATE-----";
+
+ const string keyContent = @"-----BEGIN RSA PRIVATE KEY-----
+... your encrypted private key here ...
+-----END RSA PRIVATE KEY-----";
+
+ return new X509Certificate2(
+ Encoding.UTF8.GetBytes(certContent),
+ Encoding.UTF8.GetBytes(keyContent),
+ "your-private-key-password"
+ );
+}
+```
+
+## SSL/TLS Protocol Configuration
+
+This section goes through different options for the SSL configuration.
+
+### TLS 1.2 Only (Minimum Recommended)
+
+```csharp
+server.SslProtocols = SslProtocols.Tls12;
+```
+
+### Multiple TLS Versions
+
+```csharp
+server.SslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
+```
+
+### Available Options
+
+- `SslProtocols.Tls` - TLS 1.0
+- `SslProtocols.Tls11` - TLS 1.1
+- `SslProtocols.Tls12` - TLS 1.2 (minimum recommended)
+- `SslProtocols.Tls13` - TLS 1.3
+
+## Certificate Storage Options
+
+You have 4 diufferent ways to store certificates and use them:
+
+- **Embed in code**: this is not really recommended for production as you cannot easilly replace the certificate except by flashing the device with a new code.
+- **From resources**: similar as for the in code, this solution won't allow you to easilly replace the certificate.
+- **From Visual Studio**: in the .NET nanoFramework extension, select your device, then `Edit Network Configuration`, then `General`, you can upload root CA and device CA.
+- **From storage**: typically added in the (typically internal) storage at setup time or flash time, this is suitable for production as can also be replaced more easilly.
+- **At flash time**: during the initial flash process, this can be deployed as well as the wifi credential. Also suitable for production and also easier to flash later on.
+
+### Embedded in Code
+
+```csharp
+public static class Certificates
+{
+ public const string ServerCertificate = @"-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAKL0UG+mRnNjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+... certificate content ...
+-----END CERTIFICATE-----";
+
+ public const string ServerPrivateKey = @"-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+... private key content ...
+-----END RSA PRIVATE KEY-----";
+}
+```
+
+> [!Note] You can upload mutliple certificates at once, add them one after the other one, in the same file/string each properly separated by the `BEGIN CERTIFICATE` and `END CERTIFICATE` markers.
+
+### From Resources
+
+```csharp
+// Add certificate files as embedded resources in your project
+public static X509Certificate2 LoadFromResources()
+{
+ var certBytes = Resources.GetBytes(Resources.BinaryResources.server_crt);
+ var keyBytes = Resources.GetBytes(Resources.BinaryResources.server_key);
+
+ return new X509Certificate2(certBytes, keyBytes, "password");
+}
+```
+
+### From File System
+
+```csharp
+// If your device supports file system
+public static X509Certificate2 LoadFromFiles()
+{
+ var certBytes = File.ReadAllBytes("I:\\certificates\\server.crt");
+ var keyBytes = File.ReadAllBytes("I:\\certificates\\server.key");
+
+ return new X509Certificate2(certBytes, keyBytes, "password");
+}
+```
+
+### Uploading them in the device at flash time
+
+You can upload the certificates directly into the device by following the steps described in the [naoff documentation](https://github.com/nanoframework/nanoFirmwareFlasher?tab=readme-ov-file#deploy-wireless-wireless-access-point-ethernet-configuration-and-certificates).
+
+Note that the same method can be used as well to deploy the certificated into the internal storage as well.
+
+## Production Considerations
+
+There are couple of elements to consider when creating a certificate and embedding it into your nanoFramework device.
+
+### Valid Certificate Authority
+
+For production, use certificates from a trusted Certificate Authority (CA):
+
+1. **Let's Encrypt** - Free certificates, typically used for hobbyist usage which still want some security
+2. **Commercial CAs** - Paid certificates, typically used for a device you'll sell
+3. **Internal CA** - For enterprise environments (commercial CA can also be used in enterprise environments)
+
+### Certificate Installation
+
+Because self-signed certificates aren't trusted by browsers, you may need to:
+
+1. **Install certificate** in browser/OS certificate store
+2. **Add security exception** in browser
+3. **Use proper CA-signed certificate** for production
+
+### Windows Certificate Installation
+
+1. Save the certificate as `server.crt`
+2. Double-click the file
+3. Click "Install Certificate..."
+4. Choose "Local Machine" or "Current User"
+5. Select "Trusted Root Certification Authorities"
+6. Complete the installation
+
+## MCP with HTTPS
+
+The Model Context Procol server also works with HTTPS.
+
+```csharp
+public static void Main()
+{
+ X509Certificate2 cert = LoadCertificate();
+
+ // Discover MCP tools
+ McpToolRegistry.DiscoverTools(new Type[] { typeof(IoTTools) });
+
+ using (var server = new WebServer(443, HttpProtocol.Https, new Type[] { typeof(McpServerController) }))
+ {
+ // Configure HTTPS
+ server.HttpsCert = cert;
+ server.SslProtocols = SslProtocols.Tls12;
+
+ // Configure MCP
+ McpServerController.ServerName = "SecureIoTDevice";
+ McpServerController.Instructions = "Secure IoT device accessible via HTTPS only";
+
+ server.Start();
+ Debug.WriteLine("Secure MCP server started");
+ Thread.Sleep(Timeout.Infinite);
+ }
+}
+```
+
+## Testing HTTPS
+
+Here are several ways to test your HTTPS endpoints securely and effectively:
+
+### Using REST Client VS Code Extension
+
+The [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension for VS Code provides an excellent way to test your HTTPS endpoints directly from your development environment.
+
+Create a `.http` file in your project:
+
+```http
+### Test basic HTTPS endpoint
+GET https://192.168.1.100/api/status
+Accept: application/json
+
+### Test with authentication
+GET https://192.168.1.100/secure/data
+Authorization: Basic YWRtaW46cGFzc3dvcmQ=
+Accept: application/json
+
+### Test MCP over HTTPS
+POST https://192.168.1.100/mcp
+Content-Type: application/json
+
+{
+ "jsonrpc": "2.0",
+ "method": "tools/list",
+ "id": 1
+}
+
+### Test file upload
+POST https://192.168.1.100/api/upload
+Content-Type: multipart/form-data; boundary=boundary
+
+--boundary
+Content-Disposition: form-data; name="file"; filename="test.txt"
+Content-Type: text/plain
+
+Hello World!
+--boundary--
+```
+
+**For Self-Signed Certificates**: Add these settings to your VS Code `settings.json`:
+
+```json
+{
+ "rest-client.enableTelemetry": false,
+ "rest-client.environmentVariables": {
+ "$shared": {
+ "host": "192.168.1.100"
+ }
+ },
+ "http.proxyStrictSSL": false
+}
+```
+
+### Using curl
+
+#### Secure Testing (Recommended)
+
+For production environments, always verify certificates properly:
+
+```bash
+# Test with proper certificate verification
+curl --cacert ca-certificate.pem https://192.168.1.100/api/status
+
+# If using a certificate authority bundle
+curl --capath /etc/ssl/certs https://192.168.1.100/api/status
+
+# Test with client certificate authentication
+curl --cert client.pem --key client-key.pem https://192.168.1.100/secure/data
+
+# Test with basic authentication and proper certificate verification
+curl --cacert ca-certificate.pem -u admin:password https://192.168.1.100/secure/data
+
+# MCP over HTTPS with certificate verification
+curl --cacert ca-certificate.pem -X POST https://192.168.1.100/mcp \
+ -H "Content-Type: application/json" \
+ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
+```
+
+#### Development/Testing Only
+
+**Warning**: Only use these commands during development with self-signed certificates. Never use `-k` in production:
+
+```bash
+# DEVELOPMENT ONLY: Ignore certificate verification (NOT for production)
+curl -k https://192.168.1.100/api/status
+
+# DEVELOPMENT ONLY: With authentication, ignoring certificate errors
+curl -k -u admin:password https://192.168.1.100/secure/data
+
+# DEVELOPMENT ONLY: MCP testing with certificate verification disabled
+curl -k -X POST https://192.168.1.100/mcp \
+ -H "Content-Type: application/json" \
+ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
+```
+
+#### Adding CA Certificate for Verification
+
+To properly verify your self-signed or custom certificates:
+
+**On Windows (PowerShell)**:
+
+```powershell
+# Add to Windows certificate store
+Import-Certificate -FilePath "ca-certificate.crt" -CertStoreLocation "Cert:\LocalMachine\Root"
+
+# Test after adding to store
+curl https://192.168.1.100/api/status
+```
+
+**On Linux**:
+
+```bash
+# Copy CA certificate to system store
+sudo cp ca-certificate.crt /usr/local/share/ca-certificates/
+sudo update-ca-certificates
+
+# Test with system certificates
+curl https://192.168.1.100/api/status
+```
+
+**On macOS**:
+
+```bash
+# Add to system keychain
+sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca-certificate.crt
+
+# Test with system certificates
+curl https://192.168.1.100/api/status
+```
+
+### Using Browser Developer Tools
+
+Modern browsers provide excellent tools for testing HTTPS endpoints:
+
+1. **Network Tab**: Monitor request/response details, timing, and certificate information
+2. **Security Tab**: View certificate details and validation status
+3. **Console**: Use `fetch()` API for programmatic testing
+
+```javascript
+// Test API endpoint from browser console
+fetch('https://192.168.1.100/api/status')
+ .then(response => response.json())
+ .then(data => console.log(data))
+ .catch(error => console.error('Error:', error));
+
+// Test with authentication
+fetch('https://192.168.1.100/secure/data', {
+ headers: {
+ 'Authorization': 'Basic ' + btoa('admin:password')
+ }
+})
+ .then(response => response.json())
+ .then(data => console.log(data));
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Certificate not trusted**: Install certificate in browser/OS
+2. **Wrong private key password**: Verify the password used during key encryption
+3. **Certificate format**: Ensure PEM format with proper headers
+4. **Port conflicts**: Ensure port 443 is available
+5. **Memory issues**: HTTPS uses more memory than HTTP
diff --git a/doc/model-context-protocol.md b/doc/model-context-protocol.md
new file mode 100644
index 0000000..9f65d75
--- /dev/null
+++ b/doc/model-context-protocol.md
@@ -0,0 +1,1316 @@
+# 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.
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Key Features](#key-features)
+- [Requirements](#requirements)
+- [Installation](#installation)
+- [Quick Start](#quick-start)
+- [Defining MCP Tools](#defining-mcp-tools)
+- [Complex Object Support](#complex-object-support)
+- [Server Setup](#server-setup)
+- [Authentication Options](#authentication-options)
+- [Protocol Flow](#protocol-flow)
+- [Request/Response Examples](#requestresponse-examples)
+- [Error Handling](#error-handling)
+- [Best Practices](#best-practices)
+- [Complete Examples](#complete-examples)
+- [Client Integration](#client-integration)
+- [Troubleshooting](#troubleshooting)
+
+## Overview
+
+The Model Context Protocol (MCP) is an open standard that enables seamless integration between AI applications and external data sources or tools. The nanoFramework implementation provides a lightweight, efficient MCP server that runs directly on embedded devices.
+
+### Key Features
+
+- **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
+- **Memory efficient** implementation optimized for embedded devices
+- **HTTPS support** with SSL/TLS encryption
+
+### Supported Version
+
+This implementation supports MCP protocol version **2025-03-26** as defined in the [official schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json).
+
+### Limitations
+
+- **Server features only**: No client-side features implemented
+- **No notifications**: Server-sent events and notifications are not supported
+- **Single parameter limitation**: Tools can have zero or one parameter (use classes for multiple values)
+- **Text responses only**: All tool responses are returned as text content, classes will be serialized and send as text.
+
+## Requirements
+
+- **NuGet Package**: `nanoFramework.WebServer.Mcp`
+- **Network Connectivity**: WiFi, Ethernet, or other network connection
+- **Memory**: Sufficient RAM for JSON parsing and object serialization, also reflection used is quite memory intensive
+
+## Quick Start
+
+Here's a minimal MCP server setup:
+
+```csharp
+using System;
+using System.Threading;
+using nanoFramework.WebServer;
+using nanoFramework.WebServer.Mcp;
+
+public class SimpleMcpTools
+{
+ [McpServerTool("hello", "Returns a greeting message")]
+ public static string SayHello(string name)
+ {
+ return $"Hello, {name}! Greetings from nanoFramework.";
+ }
+}
+
+public class Program
+{
+ public static void Main()
+ {
+ // Connect to WiFi (device-specific implementation)
+ // ConnectToWiFi();
+
+ // Discover and register tools
+ McpToolRegistry.DiscoverTools(new Type[] { typeof(SimpleMcpTools) });
+
+ // Start MCP server
+ using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }))
+ {
+ server.Start();
+ Console.WriteLine("MCP server running on port 80");
+ Thread.Sleep(Timeout.Infinite);
+ }
+ }
+}
+```
+
+## Defining MCP Tools
+
+### Basic Tool Definition
+
+Use the `[McpServerTool]` attribute to mark methods as MCP tools:
+
+```csharp
+public class IoTTools
+{
+ [McpServerTool("read_temperature", "Reads the current temperature from the sensor")]
+ public static string ReadTemperature()
+ {
+ // Your sensor reading implementation
+ float temperature = ReadTemperatureSensor();
+ return $"{temperature:F1}°C";
+ }
+
+ [McpServerTool("toggle_led", "Toggles the device LED on or off", "Returns the status of the led")]
+ public static string ToggleLed()
+ {
+ // Your LED control implementation
+ bool isOn = ToggleDeviceLed();
+ return $"LED is now {(isOn ? "ON" : "OFF")}";
+ }
+}
+```
+
+### Tools with Parameters
+
+Tools can accept a single parameter of any type:
+
+```csharp
+public class AdvancedTools
+{
+ [McpServerTool("set_brightness", "Sets LED brightness level")]
+ public static string SetBrightness(int level)
+ {
+ if (level < 0 || level > 100)
+ {
+ return "Error: Brightness must be between 0 and 100";
+ }
+
+ SetLedBrightness(level);
+ return $"Brightness set to {level}%";
+ }
+
+ [McpServerTool("calculate_power", "Calculates power consumption")]
+ public static string CalculatePower(float voltage)
+ {
+ float current = GetCurrentReading();
+ float power = voltage * current;
+ return $"Power: {power:F2}W (V: {voltage:F1}V, I: {current:F3}A)";
+ }
+}
+```
+
+### Tools with Output Descriptions
+
+Provide output descriptions for better AI understanding:
+
+```csharp
+public class DocumentedTools
+{
+ [McpServerTool("get_system_info", "Retrieves system information", "JSON object containing device status")]
+ public static string GetSystemInfo()
+ {
+ return "{\"device\":\"ESP32\",\"memory\":\"75%\",\"uptime\":\"2d 5h 30m\"}";
+ }
+
+ [McpServerTool("read_sensors", "Reads all available sensors", "Comma-separated sensor readings")]
+ public static string ReadAllSensors()
+ {
+ return "Temperature: 23.5°C, Humidity: 65%, Pressure: 1013 hPa";
+ }
+}
+```
+
+## Complex Object Support
+
+This MCP implementation supports complex types which can be implemented with classes and nested classes.
+
+### Defining Complex Types
+
+Use classes to handle multiple parameters or complex data structures:
+
+```csharp
+public class DeviceConfig
+{
+ [Description("Device name identifier")]
+ public string DeviceName { get; set; }
+
+ [Description("Operating mode: auto, manual, or sleep")]
+ public string Mode { get; set; }
+
+ [Description("Update interval in seconds")]
+ public int UpdateInterval { get; set; } = 60;
+
+ public WifiSettings Wifi { get; set; } = new WifiSettings();
+}
+
+public class WifiSettings
+{
+ [Description("WiFi network SSID")]
+ public string SSID { get; set; }
+
+ [Description("WiFi signal strength in dBm")]
+ public int SignalStrength { get; set; }
+
+ [Description("Connection status")]
+ public bool IsConnected { get; set; }
+}
+```
+
+### Tools with Complex Parameters
+
+This example shows how complex objects with nested classes can be handled transparently and smoothly:
+
+```csharp
+public class ConfigurationTools
+{
+ [McpServerTool("configure_device", "Updates device configuration", "Configuration update status")]
+ public static string ConfigureDevice(DeviceConfig config)
+ {
+ try
+ {
+ // Validate configuration
+ if (string.IsNullOrEmpty(config.DeviceName))
+ {
+ return "Error: Device name is required";
+ }
+
+ if (config.UpdateInterval < 10 || config.UpdateInterval > 3600)
+ {
+ return "Error: Update interval must be between 10 and 3600 seconds";
+ }
+
+ // Apply configuration
+ ApplyDeviceConfig(config);
+
+ return $"Device '{config.DeviceName}' configured successfully. Mode: {config.Mode}, Interval: {config.UpdateInterval}s";
+ }
+ catch (Exception ex)
+ {
+ return $"Configuration error: {ex.Message}";
+ }
+ }
+
+ [McpServerTool("get_wifi_status", "Retrieves WiFi connection status", "WiFi status information")]
+ public static WifiSettings GetWifiStatus()
+ {
+ return new WifiSettings
+ {
+ SSID = GetCurrentSSID(),
+ SignalStrength = GetSignalStrength(),
+ IsConnected = IsWifiConnected()
+ };
+ }
+}
+```
+
+### Nested Objects
+
+Support for deeply nested object structures and types, for example:
+
+```csharp
+public class SensorReading
+{
+ public string SensorId { get; set; }
+ public SensorData Data { get; set; }
+ public Metadata Info { get; set; }
+}
+
+public class SensorData
+{
+ public float Value { get; set; }
+ public string Unit { get; set; }
+ public DateTime Timestamp { get; set; }
+}
+
+public class Metadata
+{
+ public string Location { get; set; }
+ public CalibrationInfo Calibration { get; set; }
+}
+
+public class CalibrationInfo
+{
+ public DateTime LastCalibrated { get; set; }
+ public float Offset { get; set; }
+}
+```
+
+## Server Setup
+
+This section will go through the setup and configuration of the MCP Server.
+
+### Basic Server Configuration
+
+```csharp
+public static void Main()
+{
+ // Step 1: Connect to network
+ var connected = WifiNetworkHelper.ConnectDhcp(Ssid, Password, requiresDateTime: true);
+ if (!connected)
+ {
+ Console.WriteLine("Failed to connect to WiFi");
+ return;
+ }
+
+ // Step 2: Discover and register MCP tools
+ McpToolRegistry.DiscoverTools(new Type[] {
+ typeof(IoTTools),
+ typeof(ConfigurationTools),
+ typeof(SensorTools)
+ });
+
+ // Step 3: Start WebServer with MCP support
+ using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }))
+ {
+ server.Start();
+ Console.WriteLine($"MCP server running on http://{NetworkHelper.GetLocalIpAddress()}");
+ Thread.Sleep(Timeout.Infinite);
+ }
+}
+```
+
+### Custom Server Information
+
+Customize the server identity and instructions:
+
+```csharp
+using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }))
+{
+ // Customize server information
+ McpServerController.ServerName = "SmartThermostat";
+ McpServerController.ServerVersion = "2.1.0";
+
+ // Provide custom instructions for AI agents
+ McpServerController.Instructions = @"
+ This is a smart thermostat device with the following capabilities:
+ - Temperature and humidity monitoring
+ - HVAC system control (heating/cooling)
+ - Schedule management
+ - Energy usage tracking
+
+ Please send requests one at a time and wait for responses.
+ All temperature values are in Celsius unless specified otherwise.
+ ";
+
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+### HTTPS Configuration
+
+For secure communication, configure HTTPS. See the [HTTPS documentation](./https-certificates.md).
+
+```csharp
+// Generate or load your certificate
+X509Certificate2 certificate = LoadOrGenerateCertificate();
+
+using (var server = new WebServer(443, HttpProtocol.Https, new Type[] { typeof(McpServerController) }))
+{
+ server.HttpsCert = certificate;
+ server.SslProtocols = SslProtocols.Tls12;
+
+ McpServerController.ServerName = "SecureIoTDevice";
+
+ server.Start();
+ Console.WriteLine("Secure MCP server running on HTTPS port 443");
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+## Authentication Options
+
+### 1. No Authentication (Default)
+
+Suitable for development and trusted networks:
+
+```csharp
+using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }))
+{
+ // No authentication configuration needed
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+### 2. Basic Authentication
+
+Username and password authentication:
+
+```csharp
+using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerBasicAuthenticationController) }))
+{
+ // Set default credentials
+ server.Credential = new NetworkCredential("admin", "securepassword123");
+
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+### 3. API Key Authentication
+
+Token-based authentication:
+
+```csharp
+using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerKeyAuthenticationController) }))
+{
+ // Set API key
+ server.ApiKey = "mcp-key-abc123def456ghi789";
+
+ server.Start();
+ Thread.Sleep(Timeout.Infinite);
+}
+```
+
+Note that any authentication can be combined with HTTPS.
+
+### Authentication in Client Requests
+
+When authentication is enabled, clients must include credentials:
+
+**Basic Authentication:**
+
+```http
+POST /mcp HTTP/1.1
+Authorization: Basic YWRtaW46c2VjdXJlcGFzc3dvcmQxMjM=
+Content-Type: application/json
+
+{"jsonrpc":"2.0","method":"tools/list","id":1}
+```
+
+**API Key Authentication:**
+
+```http
+POST /mcp HTTP/1.1
+ApiKey: mcp-key-abc123def456ghi789
+Content-Type: application/json
+
+{"jsonrpc":"2.0","method":"tools/list","id":1}
+```
+
+## Protocol Flow
+
+### 1. Initialization
+
+AI agent establishes connection with the MCP server:
+
+```json
+POST /mcp
+{
+ "jsonrpc": "2.0",
+ "method": "initialize",
+ "params": {
+ "protocolVersion": "2025-03-26",
+ "capabilities": {},
+ "clientInfo": {
+ "name": "AI Assistant",
+ "version": "1.0.0"
+ }
+ },
+ "id": 1
+}
+```
+
+**Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "protocolVersion": "2025-03-26",
+ "capabilities": {
+ "tools": {}
+ },
+ "serverInfo": {
+ "name": "SmartThermostat",
+ "version": "2.1.0"
+ },
+ "instructions": "This is a smart thermostat device..."
+ }
+}
+```
+
+### 2. Tool Discovery
+
+Agent discovers available tools:
+
+```json
+POST /mcp
+{
+ "jsonrpc": "2.0",
+ "method": "tools/list",
+ "id": 2
+}
+```
+
+The response will be the list of the tools. See next section for detailed examples.
+
+### 3. Tool Invocation
+
+Agent calls specific tools with parameters:
+
+```json
+POST /mcp
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "set_temperature",
+ "arguments": {
+ "target": 22.5,
+ "mode": "heat"
+ }
+ },
+ "id": 3
+}
+```
+
+## Request/Response Examples
+
+This section shows real exampled of requests and responses.
+
+### Tool Discovery
+
+**Request:**
+
+```json
+POST /mcp
+
+{
+ "jsonrpc": "2.0",
+ "method": "tools/list",
+ "id": 1
+}
+```
+
+**Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "tools": [
+ {
+ "name": "read_temperature",
+ "description": "Reads the current temperature from the sensor",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": []
+ }
+ },
+ {
+ "name": "set_brightness",
+ "description": "Sets LED brightness level",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "number",
+ "description": "Input parameter of type Int32"
+ }
+ },
+ "required": []
+ }
+ },
+ {
+ "name": "configure_device",
+ "description": "Updates device configuration",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "DeviceName": {
+ "type": "string",
+ "description": "Device name identifier"
+ },
+ "Mode": {
+ "type": "string",
+ "description": "Operating mode: auto, manual, or sleep"
+ },
+ "UpdateInterval": {
+ "type": "number",
+ "description": "Update interval in seconds"
+ },
+ "Wifi": {
+ "type": "object",
+ "properties": {
+ "SSID": {
+ "type": "string",
+ "description": "WiFi network SSID"
+ },
+ "SignalStrength": {
+ "type": "number",
+ "description": "WiFi signal strength in dBm"
+ },
+ "IsConnected": {
+ "type": "boolean",
+ "description": "Connection status"
+ }
+ }
+ }
+ },
+ "required": []
+ }
+ }
+ ],
+ "nextCursor": null
+ }
+}
+```
+
+### Simple Tool Invocation
+
+**Request:**
+
+```json
+POST /mcp
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "read_temperature",
+ "arguments": {}
+ },
+ "id": 2
+}
+```
+
+**Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "result": {
+ "content": [
+ {
+ "type": "text",
+ "text": "23.5°C"
+ }
+ ]
+ }
+}
+```
+
+### Complex Tool Invocation
+
+**Request:**
+
+```json
+POST /mcp
+
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "configure_device",
+ "arguments": {
+ "DeviceName": "Thermostat-01",
+ "Mode": "auto",
+ "UpdateInterval": 120,
+ "Wifi": {
+ "SSID": "HomeNetwork",
+ "SignalStrength": -45,
+ "IsConnected": true
+ }
+ }
+ },
+ "id": 3
+}
+```
+
+Note that in most cases LLM will send the payload where numbers are integrated into strings. The nanoFramework MCP server knows how to deal with this and will always cast to the proper type:
+
+```json
+POST /mcp
+
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "configure_device",
+ "arguments": {
+ "DeviceName": "Thermostat-01",
+ "Mode": "auto",
+ "UpdateInterval": "120",
+ "Wifi": {
+ "SSID": "HomeNetwork",
+ "SignalStrength": "-45",
+ "IsConnected": "true"
+ }
+ }
+ },
+ "id": 3
+}
+```
+
+**Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 3,
+ "result": {
+ "content": [
+ {
+ "type": "text",
+ "text": "Device 'Thermostat-01' configured successfully. Mode: auto, Interval: 120s"
+ }
+ ]
+ }
+}
+```
+
+## Error Handling
+
+The .NET nanoFramework MCP Server knows how to handle properly errors. The following will show examples of request and error responses.
+
+### Protocol Version Mismatch
+
+**Request with unsupported version:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "initialize",
+ "params": {
+ "protocolVersion": "1.0.0"
+ },
+ "id": 1
+}
+```
+
+**Error Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "error": {
+ "code": -32602,
+ "message": "Unsupported protocol version",
+ "data": {
+ "supported": ["2025-03-26"],
+ "requested": "1.0.0"
+ }
+ }
+}
+```
+
+### Tool Not Found
+
+**Request:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "nonexistent_tool",
+ "arguments": {}
+ },
+ "id": 4
+}
+```
+
+**Error Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 4,
+ "error": {
+ "code": -32601,
+ "message": "Tool 'nonexistent_tool' not found"
+ }
+}
+```
+
+### Invalid Method
+
+**Request:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "invalid/method",
+ "id": 5
+}
+```
+
+**Error Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 5,
+ "error": {
+ "code": -32601,
+ "message": "Method not found"
+ }
+}
+```
+
+### Common Error Codes
+
+| Code | Description | Common Causes |
+|------|-------------|---------------|
+| -32600 | Invalid Request | Malformed JSON-RPC |
+| -32601 | Method Not Found | Unknown method or tool |
+| -32602 | Invalid Params | Wrong parameters or unsupported protocol version |
+| -32603 | Internal Error | Server-side errors |
+
+## Best Practices
+
+### Tool Design
+
+1. **Single Responsibility**: Each tool should have one clear purpose
+2. **Descriptive Names**: Use clear, descriptive tool names
+3. **Comprehensive Descriptions**: Provide detailed descriptions for tools and parameters
+4. **Error Handling**: Implement proper validation and error reporting
+5. **Consistent Return Types**: Use consistent response formats
+
+```csharp
+public class WellDesignedTools
+{
+ [McpServerTool("measure_distance", "Measures distance using ultrasonic sensor", "Distance in centimeters")]
+ public static string MeasureDistance()
+ {
+ try
+ {
+ float distance = UltrasonicSensor.GetDistance();
+
+ if (distance < 0)
+ {
+ return "Error: Sensor reading failed";
+ }
+
+ if (distance > 400)
+ {
+ return "Out of range (max 400cm)";
+ }
+
+ return $"{distance:F1} cm";
+ }
+ catch (Exception ex)
+ {
+ return $"Sensor error: {ex.Message}";
+ }
+ }
+}
+```
+
+### Performance Optimization
+
+1. **Memory Management**: Be mindful of memory usage on embedded devices
+2. **Efficient Serialization**: Keep JSON payloads small
+3. **Caching**: Cache frequently accessed data
+4. **Async Operations**: Use appropriate patterns for long-running operations
+
+```csharp
+public class OptimizedTools
+{
+ private static string _cachedSystemInfo;
+ private static DateTime _lastInfoUpdate;
+
+ [McpServerTool("get_cached_info", "Gets cached system information")]
+ public static string GetCachedSystemInfo()
+ {
+ // Cache system info for 30 seconds
+ if (_cachedSystemInfo == null || DateTime.UtcNow - _lastInfoUpdate > TimeSpan.FromSeconds(30))
+ {
+ _cachedSystemInfo = GenerateSystemInfo();
+ _lastInfoUpdate = DateTime.UtcNow;
+ }
+
+ return _cachedSystemInfo;
+ }
+}
+```
+
+### Security Considerations
+
+1. **Authentication**: Use appropriate authentication for your security requirements
+2. **Input Validation**: Always validate tool parameters
+3. **Rate Limiting**: Consider implementing rate limiting for sensitive operations
+4. **HTTPS**: Use HTTPS for production deployments
+
+```csharp
+public class SecureTools
+{
+ private static DateTime _lastResetTime = DateTime.MinValue;
+
+ [McpServerTool("factory_reset", "Performs factory reset (requires confirmation)")]
+ public static string FactoryReset(ResetRequest request)
+ {
+ // Prevent frequent resets
+ if (DateTime.UtcNow - _lastResetTime < TimeSpan.FromMinutes(10))
+ {
+ return "Error: Factory reset was performed recently. Please wait 10 minutes.";
+ }
+
+ // Validate confirmation
+ if (request.ConfirmationCode != "FACTORY_RESET_CONFIRMED")
+ {
+ return "Error: Invalid confirmation code";
+ }
+
+ // Additional validation
+ if (string.IsNullOrEmpty(request.Reason))
+ {
+ return "Error: Reset reason is required";
+ }
+
+ _lastResetTime = DateTime.UtcNow;
+ PerformFactoryReset();
+
+ return "Factory reset completed successfully";
+ }
+}
+
+public class ResetRequest
+{
+ [Description("Confirmation code (must be 'FACTORY_RESET_CONFIRMED')")]
+ public string ConfirmationCode { get; set; }
+
+ [Description("Reason for factory reset")]
+ public string Reason { get; set; }
+}
+```
+
+## Complete Examples
+
+### Smart Thermostat
+
+A complete thermostat implementation with multiple tools:
+
+```csharp
+using System;
+using System.Threading;
+using nanoFramework.WebServer;
+using nanoFramework.WebServer.Mcp;
+
+public class ThermostatConfig
+{
+ [Description("Target temperature in Celsius")]
+ public float TargetTemperature { get; set; } = 22.0f;
+
+ [Description("Operating mode: heat, cool, auto, or off")]
+ public string Mode { get; set; } = "auto";
+
+ [Description("Enable schedule-based operation")]
+ public bool ScheduleEnabled { get; set; } = true;
+}
+
+public class ThermostatStatus
+{
+ public float CurrentTemperature { get; set; }
+ public float TargetTemperature { get; set; }
+ public string Mode { get; set; }
+ public bool IsHeating { get; set; }
+ public bool IsCooling { get; set; }
+ public float Humidity { get; set; }
+ public DateTime LastUpdate { get; set; }
+}
+
+public class ThermostatTools
+{
+ private static ThermostatConfig _config = new ThermostatConfig();
+ private static bool _isHeating = false;
+ private static bool _isCooling = false;
+
+ [McpServerTool("get_temperature", "Reads current temperature and humidity")]
+ public static string GetTemperature()
+ {
+ float temp = ReadTemperatureSensor();
+ float humidity = ReadHumiditySensor();
+
+ return $"Temperature: {temp:F1}°C, Humidity: {humidity:F0}%";
+ }
+
+ [McpServerTool("set_target_temperature", "Sets the target temperature")]
+ public static string SetTargetTemperature(float temperature)
+ {
+ if (temperature < 5 || temperature > 35)
+ {
+ return "Error: Temperature must be between 5°C and 35°C";
+ }
+
+ _config.TargetTemperature = temperature;
+ UpdateThermostatControl();
+
+ return $"Target temperature set to {temperature:F1}°C";
+ }
+
+ [McpServerTool("configure_thermostat", "Updates thermostat configuration")]
+ public static string ConfigureThermostat(ThermostatConfig config)
+ {
+ if (config.TargetTemperature < 5 || config.TargetTemperature > 35)
+ {
+ return "Error: Target temperature must be between 5°C and 35°C";
+ }
+
+ if (config.Mode != "heat" && config.Mode != "cool" && config.Mode != "auto" && config.Mode != "off")
+ {
+ return "Error: Mode must be 'heat', 'cool', 'auto', or 'off'";
+ }
+
+ _config = config;
+ UpdateThermostatControl();
+
+ return $"Thermostat configured: {config.TargetTemperature:F1}°C, Mode: {config.Mode}";
+ }
+
+ [McpServerTool("get_status", "Gets complete thermostat status", "JSON object with thermostat status")]
+ public static ThermostatStatus GetStatus()
+ {
+ return new ThermostatStatus
+ {
+ CurrentTemperature = ReadTemperatureSensor(),
+ TargetTemperature = _config.TargetTemperature,
+ Mode = _config.Mode,
+ IsHeating = _isHeating,
+ IsCooling = _isCooling,
+ Humidity = ReadHumiditySensor(),
+ LastUpdate = DateTime.UtcNow
+ };
+ }
+
+ private static float ReadTemperatureSensor()
+ {
+ // Simulate sensor reading
+ return 23.5f + (float)(new Random().NextDouble() - 0.5) * 2;
+ }
+
+ private static float ReadHumiditySensor()
+ {
+ // Simulate sensor reading
+ return 65f + (float)(new Random().NextDouble() - 0.5) * 10;
+ }
+
+ private static void UpdateThermostatControl()
+ {
+ float currentTemp = ReadTemperatureSensor();
+
+ switch (_config.Mode.ToLower())
+ {
+ case "heat":
+ _isHeating = currentTemp < _config.TargetTemperature - 0.5f;
+ _isCooling = false;
+ break;
+ case "cool":
+ _isHeating = false;
+ _isCooling = currentTemp > _config.TargetTemperature + 0.5f;
+ break;
+ case "auto":
+ _isHeating = currentTemp < _config.TargetTemperature - 1.0f;
+ _isCooling = currentTemp > _config.TargetTemperature + 1.0f;
+ break;
+ case "off":
+ _isHeating = false;
+ _isCooling = false;
+ break;
+ }
+ }
+}
+
+public class Program
+{
+ private const string Ssid = "YourWiFiSSID";
+ private const string Password = "YourWiFiPassword";
+
+ public static void Main()
+ {
+ Console.WriteLine("Starting Smart Thermostat MCP Server...");
+
+ // Connect to WiFi
+ var connected = WifiNetworkHelper.ConnectDhcp(Ssid, Password, requiresDateTime: true);
+ if (!connected)
+ {
+ Console.WriteLine("Failed to connect to WiFi");
+ return;
+ }
+
+ Console.WriteLine($"Connected to WiFi. IP: {GetCurrentIPAddress()}");
+
+ // Register MCP tools
+ McpToolRegistry.DiscoverTools(new Type[] { typeof(ThermostatTools) });
+ Console.WriteLine("Thermostat tools registered");
+
+ // Start MCP server
+ using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(McpServerController) }))
+ {
+ McpServerController.ServerName = "SmartThermostat";
+ McpServerController.ServerVersion = "1.0.0";
+ McpServerController.Instructions = @"
+ Smart Thermostat with the following capabilities:
+ - Temperature and humidity monitoring
+ - Target temperature control (5°C to 35°C)
+ - Operating modes: heat, cool, auto, off
+ - Real-time status reporting
+
+ All temperatures are in Celsius.
+ ";
+
+ server.Start();
+ Console.WriteLine("Smart Thermostat MCP server is running!");
+ Console.WriteLine($"Access via: http://{NetworkHelper.GetLocalIpAddress()}/mcp");
+
+ Thread.Sleep(Timeout.Infinite);
+ }
+ }
+
+ private static string GetCurrentIPAddress()
+ {
+ NetworkInterface ni = NetworkInterface.GetAllNetworkInterfaces()[0];
+
+ // get first NI ( Wifi on ESP32 )
+ return ni.IPv4Address.ToString();
+ }
+}
+```
+
+## Client Integration
+
+### .NET MCP Client
+
+The repository includes a [.NET 10 MCP client example](../tests/McpClientTest/) that demonstrates integration with Azure OpenAI:
+
+```csharp
+using Microsoft.SemanticKernel;
+using ModelContextProtocol.Client;
+
+// Connect to nanoFramework MCP server
+var mcpClient = await McpClientFactory.CreateAsync(
+ new SseClientTransport(new SseClientTransportOptions()
+ {
+ Endpoint = new Uri("http://192.168.1.100/mcp"), // Your device IP
+ TransportMode = HttpTransportMode.StreamableHttp,
+ }, new HttpClient()));
+
+// Initialize the connection
+await mcpClient.InitializeAsync();
+
+// Discover available tools
+var tools = await mcpClient.ListToolsAsync();
+Console.WriteLine($"Discovered {tools.Length} tools");
+
+// Create Semantic Kernel and register tools
+var kernel = Kernel.CreateBuilder()
+ .AddAzureOpenAIChatCompletion("gpt-4", endpoint, apiKey)
+ .Build();
+
+// Register MCP tools as kernel functions
+kernel.Plugins.AddFromFunctions("ThermostatTools",
+ tools.Select(tool => tool.AsKernelFunction()));
+
+// Use AI with device tools
+var response = await kernel.InvokePromptAsync(
+ "What's the current temperature and set it to 24 degrees?");
+
+Console.WriteLine(response);
+```
+
+### Python MCP Client
+
+Example Python client using the official MCP SDK:
+
+```python
+import asyncio
+from mcp import Client
+from mcp.client.transport.http import HttpTransport
+
+async def main():
+ # Connect to nanoFramework device
+ transport = HttpTransport("http://192.168.1.100/mcp")
+ client = Client(transport)
+
+ # Initialize connection
+ await client.connect()
+
+ # List available tools
+ tools = await client.list_tools()
+ print(f"Available tools: {[tool.name for tool in tools]}")
+
+ # Call a tool
+ result = await client.call_tool("get_temperature", {})
+ print(f"Temperature: {result.content[0].text}")
+
+ # Configure thermostat
+ config_result = await client.call_tool("configure_thermostat", {
+ "TargetTemperature": 24.0,
+ "Mode": "auto",
+ "ScheduleEnabled": True
+ })
+ print(f"Configuration: {config_result.content[0].text}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Connection Refused**
+ - Verify WiFi connection
+ - Check IP address and port
+ - Ensure you are using proper IP and port
+
+2. **Tool Not Found**
+ - Verify tool is properly decorated with `[McpServerTool]`
+ - Check that the class is included in `DiscoverTools()`
+ - Ensure method is public and static (or instance if using instance methods)
+ - Ensure you have only 0 or 1 parameter to the function
+
+3. **Authentication Errors**
+ - Verify credentials are correctly configured
+ - Check authentication headers in requests
+ - Ensure correct authentication controller is used
+
+4. **Memory Issues**
+ - Monitor device memory usage
+ - Consider reducing JSON payload sizes
+ - Implement caching for frequently accessed data (this consue memory as well)
+
+### Debug Tips
+
+Enable detailed logging:
+
+```csharp
+public class DebuggingTools
+{
+ [McpServerTool("debug_info", "Gets debugging information")]
+ public static string GetDebugInfo()
+ {
+ var info = new
+ {
+ FreeMemory = GC.GetTotalMemory(false),
+ UpTime = DateTime.UtcNow - _startTime,
+ RequestCount = _requestCount,
+ LastError = _lastError ?? "None"
+ };
+
+ return JsonConvert.SerializeObject(info);
+ }
+
+ private static DateTime _startTime = DateTime.UtcNow;
+ private static int _requestCount = 0;
+ private static string _lastError = null;
+}
+```
+
+### Testing with HTTP Tools
+
+Use tools like curl or VS Code REST Client (adjust your local IP address):
+
+```http
+### Test tool discovery
+POST http://192.168.1.100/mcp
+Content-Type: application/json
+
+{
+ "jsonrpc": "2.0",
+ "method": "tools/list",
+ "id": 1
+}
+
+### Test tool invocation
+POST http://192.168.1.100/mcp
+Content-Type: application/json
+
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "get_temperature",
+ "arguments": {}
+ },
+ "id": 2
+}
+```
+
+### Performance Monitoring
+
+Monitor key metrics:
+
+```csharp
+public class PerformanceTools
+{
+ private static int _totalRequests = 0;
+ private static TimeSpan _totalProcessingTime = TimeSpan.Zero;
+
+ [McpServerTool("get_performance", "Gets performance metrics")]
+ public static string GetPerformanceMetrics()
+ {
+ var avgProcessingTime = _totalRequests > 0
+ ? _totalProcessingTime.TotalMilliseconds / _totalRequests
+ : 0;
+
+ return $"Requests: {_totalRequests}, Avg Time: {avgProcessingTime:F2}ms, Free Memory: {GC.GetTotalMemory(false)} bytes";
+ }
+}
+```
+
+## Related Resources
+
+- [MCP Official Specification](https://github.com/modelcontextprotocol/modelcontextprotocol)
+- [WebServer Authentication Guide](./authentication.md)
+- [HTTPS Configuration](./https-certificates.md)
+- [E2E Test Examples](../tests/McpEndToEndTest/)
+- [.NET Client Example](../tests/McpClientTest/)
+
+The Model Context Protocol support in nanoFramework WebServer enables powerful AI-device interactions, making embedded systems accessible to modern AI applications and opening new possibilities for intelligent IoT solutions.
diff --git a/doc/rest-api.md b/doc/rest-api.md
new file mode 100644
index 0000000..2044daa
--- /dev/null
+++ b/doc/rest-api.md
@@ -0,0 +1,1794 @@
+# REST API Development
+
+The nanoFramework WebServer provides comprehensive support for building RESTful APIs on embedded devices. This guide covers everything from basic API endpoints to advanced features like parameter handling, content negotiation, and error responses.
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Quick Start](#quick-start)
+- [HTTP Methods](#http-methods)
+- [URL Parameters](#url-parameters)
+- [Request Body Handling](#request-body-handling)
+- [Response Formats](#response-formats)
+- [Error Handling](#error-handling)
+- [Content Types](#content-types)
+- [Authentication](#authentication)
+- [Advanced Examples](#advanced-examples)
+- [Best Practices](#best-practices)
+- [Testing](#testing)
+
+## Overview
+
+REST (Representational State Transfer) APIs enable communication between clients and your nanoFramework device using standard HTTP methods and JSON data exchange. The WebServer provides built-in support for:
+
+- **HTTP Methods**: GET, POST, PUT, DELETE, PATCH, etc.
+- **Parameter Extraction**: URL parameters, query strings, and request bodies
+- **Content Negotiation**: JSON, XML, plain text, and custom formats
+- **Status Codes**: Standard HTTP response codes
+- **Authentication**: Basic Auth, API Keys, and custom schemes
+- **Error Handling**: Structured error responses
+
+## Quick Start
+
+### Basic API Controller
+
+To have more information about controllers, routes, method and authentication, check out the [specific Controller documentation](./controllers-routing.md).
+
+```csharp
+using System;
+using System.Net;
+using nanoFramework.WebServer;
+
+public class ApiController
+{
+ [Route("api/status")]
+ [Method("GET")]
+ public void GetStatus(WebServerEventArgs e)
+ {
+ var response = new GetResponse()
+ {
+ Status = "running",
+ Timestamp = DateTime.UtcNow.ToString(),
+ Uptime = "2d 5h 30m"
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+
+ [Route("api/hello")]
+ [Method("POST")]
+ public void PostHello(WebServerEventArgs e)
+ {
+ if (e.Context.Request.ContentLength64 > 0)
+ {
+ var body = e.Context.Request.ReadBody();
+ var content = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+
+ var response = $"{{\"message\":\"Hello, {content}!\"}}";
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, response);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ }
+ }
+}
+```
+
+### Starting the API Server
+
+This is the minimal code requiring a valid network connectin through ethernet or wifi.
+
+```csharp
+public static void Main()
+{
+ // Connect to a network. This is device specific.
+
+ using (var server = new WebServer(80, HttpProtocol.Http, new Type[] { typeof(ApiController) }))
+ {
+ server.Start();
+ Console.WriteLine("REST API server running on port 80");
+ Thread.Sleep(Timeout.Infinite);
+ }
+}
+```
+
+## HTTP Methods
+
+This section will provide various example of methods and routes.
+
+### Data Models
+
+First, let's define the data classes used in our examples:
+
+```csharp
+public class DeviceInfo
+{
+ public string DeviceId { get; set; }
+ public string Model { get; set; }
+ public string Firmware { get; set; }
+ public long FreeMemory { get; set; }
+ public TimeSpan Uptime { get; set; }
+ public string Status { get; set; }
+ public DateTime LastSeen { get; set; }
+ public string Location { get; set; }
+
+ public DeviceInfo()
+ {
+ Status = "unknown";
+ LastSeen = DateTime.UtcNow;
+ Location = "unknown";
+ }
+
+ public DeviceInfo(string deviceId, string model, string firmware)
+ {
+ DeviceId = deviceId;
+ Model = model;
+ Firmware = firmware;
+ Status = "online";
+ LastSeen = DateTime.UtcNow;
+ Location = "default";
+ FreeMemory = GC.GetTotalMemory(false);
+ }
+}
+
+public class Sensor
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public double Value { get; set; }
+ public string Unit { get; set; }
+ public DateTime Timestamp { get; set; }
+ public bool IsActive { get; set; }
+ public double MinValue { get; set; }
+ public double MaxValue { get; set; }
+ public string SensorType { get; set; }
+
+ public Sensor()
+ {
+ Timestamp = DateTime.UtcNow;
+ IsActive = true;
+ MinValue = double.MinValue;
+ MaxValue = double.MaxValue;
+ SensorType = "generic";
+ }
+
+ public Sensor(int id, string name, double value, string unit)
+ {
+ Id = id;
+ Name = name;
+ Value = value;
+ Unit = unit;
+ Timestamp = DateTime.UtcNow;
+ IsActive = true;
+ MinValue = double.MinValue;
+ MaxValue = double.MaxValue;
+ SensorType = "generic";
+ }
+
+ public Sensor(int id, string name, double value, string unit, string sensorType, double minValue, double maxValue)
+ {
+ Id = id;
+ Name = name;
+ Value = value;
+ Unit = unit;
+ SensorType = sensorType;
+ MinValue = minValue;
+ MaxValue = maxValue;
+ Timestamp = DateTime.UtcNow;
+ IsActive = true;
+ }
+
+ public bool IsValueInRange()
+ {
+ return Value >= MinValue && Value <= MaxValue;
+ }
+
+ public void UpdateValue(double newValue)
+ {
+ Value = newValue;
+ Timestamp = DateTime.UtcNow;
+ }
+}
+```
+
+This is just an example. For best unit management, it is recommended to use [UnitsNet](https://github.com/angularsen/UnitsNet). UnitsNet is supported by .NET nanoFramework. Each unit is packaged as a separated nuget.
+
+### GET - Retrieve Data
+
+```csharp
+public class DeviceController
+{
+ [Route("api/device/info")]
+ [Method("GET")]
+ public void GetDeviceInfo(WebServerEventArgs e)
+ {
+ var deviceInfo = new DeviceInfo()
+ {
+ DeviceId = "ESP32-001",
+ Model = "ESP32-WROOM-32",
+ Firmware = "1.2.3",
+ FreeMemory = GC.GetTotalMemory(false),
+ Uptime = DateTime.UtcNow - _startTime
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(deviceInfo));
+ }
+
+ [Route("api/sensors")]
+ [Method("GET")]
+ public void GetSensors(WebServerEventArgs e)
+ {
+ var sensors = new Sensor[]
+ {
+ new Sensor() { Id = 1, Name = "Temperature", Value = 23.5, Unit = "°C" },
+ new Sensor() { Id = 2, Name = "Humidity", Value = 65.2, Unit = "%" },
+ new Sensor() { Id = 3, Name = "Pressure", Value = 1013.25, Unit = "hPa" }
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(sensors));
+ }
+
+ private static DateTime _startTime = DateTime.UtcNow;
+}
+```
+
+### POST - Create Data
+
+```csharp
+public class ConfigController
+{
+ private static DateTime _startTime = DateTime.UtcNow;
+ private static DeviceInfo _deviceInfo;
+ private static Sensor[] _sensors;
+
+ static DeviceController()
+ {
+ // Initialize device info
+ _deviceInfo = new DeviceInfo("ESP32-001", "ESP32-WROOM-32", "1.2.3")
+ {
+ Location = "IoT Lab",
+ Status = "online"
+ };
+
+ // Initialize sensors
+ _sensors = new Sensor[]
+ {
+ new Sensor(1, "Temperature", 23.5, "°C", "environmental", -40, 85),
+ new Sensor(2, "Humidity", 65.2, "%", "environmental", 0, 100),
+ new Sensor(3, "Pressure", 1013.25, "hPa", "environmental", 300, 1100),
+ new Sensor(4, "Light", 450, "lux", "optical", 0, 100000),
+ new Sensor(5, "Motion", 0, "bool", "digital", 0, 1)
+ };
+ }
+
+ [Route("api/config")]
+ [Method("POST")]
+ public void CreateConfig(WebServerEventArgs e)
+ {
+ try
+ {
+ if (e.Context.Request.ContentLength64 == 0)
+ {
+ var error = $"{{\"error\":\"Request body is required\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var config = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ // Validate required fields
+ if (!config.Contains("name") || !config.Contains("value"))
+ {
+ var error = $"{{\"error\":\"Missing required fields: name, value\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ // Store configuration (implement your storage logic)
+ StoreConfiguration(config["name"].ToString(), config["value"].ToString());
+
+ var response = new CreateResponse()
+ {
+ Message = "Configuration created successfully",
+ Id = GenerateConfigId(),
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.Created;
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ catch (Exception ex)
+ {
+ var error = $"{{\"error\":\"Internal server error: {ex.Message}\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+
+ private void StoreConfiguration(string name, string value)
+ {
+ // Implement your configuration storage logic
+ // Typically in a Hashtable with nanoFramework
+ }
+
+ private string GenerateConfigId()
+ {
+ return Guid.NewGuid().ToString();
+ }
+}
+```
+
+### PUT - Update Data
+
+```csharp
+public class LedController
+{
+ [Route("api/led")]
+ [Method("PUT")]
+ public void UpdateLed(WebServerEventArgs e)
+ {
+ try
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var request = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ if (!request.Contains("state"))
+ {
+ var error = $"{{\"error\":\"Missing 'state' field\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ var state = request["state"].ToString().ToLower();
+ var brightness = request.Contains("brightness") ?
+ Convert.ToInt32(request["brightness"]) : 100;
+
+ if (state != "on" && state != "off")
+ {
+ var error = $"{{\"error\":\"State must be 'on' or 'off'\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ // Update LED (implement your GPIO logic)
+ UpdateLedState(state == "on", brightness);
+
+ var response = new UpdateResponse()
+ {
+ Message = "LED updated successfully",
+ State = state,
+ Brightness = brightness,
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ catch (Exception ex)
+ {
+ var error = $"{{\"error\":\"Failed to update LED: {ex.Message}\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+
+ private void UpdateLedState(bool isOn, int brightness)
+ {
+ // Implement your LED control logic
+ }
+}
+```
+
+### DELETE - Remove Data
+
+```csharp
+public class DataController
+{
+ private static Hashtable _dataStore = new Hashtable();
+
+ public class DeleteResponse
+ {
+ public string Message { get; set; }
+ public int Count { get; set; }
+ public string Timestamp { get; set; }
+ }
+
+ // Delete all data
+ [Route("api/data")]
+ [Method("DELETE")]
+ public void DeleteAllData(WebServerEventArgs e)
+ {
+ var count = _dataStore.Count;
+ _dataStore.Clear();
+
+ var response = new DeleteResponse()
+ {
+ Message = "All data deleted successfully",
+ Count = count,
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+
+ // Delete specific item by ID using parameterized route
+ [Route("api/data/{id}")]
+ [Method("DELETE")]
+ public void DeleteDataById(WebServerEventArgs e)
+ {
+ string id = e.GetRouteParameter("id");
+
+ if (_dataStore.Contains(id))
+ {
+ _dataStore.Remove(id);
+
+ var response = new DeleteResponse()
+ {
+ Message = $"Data with id '{id}' deleted successfully",
+ Count = 1,
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, response);
+ }
+ else
+ {
+ var error = $"{{\"error\":\"Data with id '{id}' not found\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+}
+```
+
+## URL Parameters
+
+.NET nanoFramework WebServer allows you to get access to all the parameters passed in the URL or the URL itself with parameters in the path.
+
+### Path Parameters
+
+#### Parameterized Routes
+
+Use parameterized routes with named placeholders for cleaner, more maintainable code:
+
+```csharp
+public class UserController
+{
+ public class User
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Email { get; set; }
+ }
+
+ public class UserSettingResponse
+ {
+ public string UserId { get; set; }
+ public string SettingName { get; set; }
+ public string Value { get; set; }
+ }
+
+ [Route("api/users/{id}")]
+ [Method("GET")]
+ public void GetUser(WebServerEventArgs e)
+ {
+ string userId = e.GetRouteParameter("id");
+
+ // Simulate user lookup
+ var user = GetUserById(userId);
+
+ if (user != null)
+ {
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(user));
+ }
+ else
+ {
+ var error = $"{{\"error\":\"User {userId} not found\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+
+ [Route("api/users/{userId}/settings/{settingName}")]
+ [Method("GET")]
+ public void GetUserSetting(WebServerEventArgs e)
+ {
+ string userId = e.GetRouteParameter("userId");
+ string settingName = e.GetRouteParameter("settingName");
+
+ var setting = GetUserSetting(userId, settingName);
+ if (setting != null)
+ {
+ var response = new UserSettingResponse()
+ {
+ UserId = userId,
+ SettingName = settingName,
+ Value = setting
+ };
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ else
+ {
+ var error = $"{{\"error\":\"Setting {settingName} not found for user {userId}\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+
+ private User GetUserById(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ return null;
+ }
+
+ // Implement user lookup logic
+ return new User() { Id = id, Name = "John Doe", Email = "john@example.com" };
+ }
+
+ private string GetUserSetting(string userId, string setting)
+ {
+ // Implement setting lookup logic
+ return "default_value";
+ }
+}
+```
+
+### Query Parameters
+
+Handle URL query parameters:
+
+```csharp
+public class SearchController
+{
+ public class SearchResponse
+ {
+ public string Query { get; set; }
+ public int Total { get; set; }
+ public int Limit { get; set; }
+ public int Offset { get; set; }
+ public string SortBy { get; set; }
+ public object[] Results { get; set; }
+ }
+
+ [Route("api/search")]
+ [Method("GET")]
+ public void Search(WebServerEventArgs e)
+ {
+ var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);
+
+ string query = "";
+ int limit = 10;
+ int offset = 0;
+ string sortBy = "relevance";
+
+ // Extract query parameters
+ foreach (var param in parameters)
+ {
+ switch (param.Name.ToLower())
+ {
+ case "q":
+ case "query":
+ query = param.Value;
+ break;
+ case "limit":
+ if (int.TryParse(param.Value, out int parsedLimit))
+ limit = Math.Min(parsedLimit, 100); // Max 100 results
+ break;
+ case "offset":
+ if (int.TryParse(param.Value, out int parsedOffset))
+ offset = Math.Max(parsedOffset, 0);
+ break;
+ case "sort":
+ sortBy = param.Value;
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(query))
+ {
+ var error = $"{{\"error\":\"Query parameter 'q' is required\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ // Perform search
+ var results = PerformSearch(query, limit, offset, sortBy);
+
+ var response = new SearchResponse()
+ {
+ Query = query,
+ Total = results.Length,
+ Limit = limit,
+ Offset = offset,
+ SortBy = sortBy,
+ Results = results
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+
+ private object[] PerformSearch(string query, int limit, int offset, string sortBy)
+ {
+ // Implement search logic
+ return new object[]
+ {
+ { "\"id\": 1, \"title\":\"Result 1\", \"score\": 0.95" },
+ { "\"id\": 2, \"title\":\"Result 2\", \"score\": 0.8" },
+ };
+ }
+}
+```
+
+## Request Body Handling
+
+This section will explain how to handle forms submissions.
+
+### JSON Request Bodies
+
+```csharp
+public class ProductController
+{
+ public class ProductCreationResponse
+ {
+ public string Message { get; set; }
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public decimal Price { get; set; }
+ public string Category { get; set; }
+ public string Timestamp { get; set;}
+ }
+
+ [Route("api/products")]
+ [Method("POST")]
+ public void CreateProduct(WebServerEventArgs e)
+ {
+ try
+ {
+ // Check content type
+ var contentType = e.Context.Request.Headers?.GetValues("Content-Type")?[0];
+ if (contentType != "application/json")
+ {
+ var error = $"{{\"error\":\"Content-Type must be application/json\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.UnsupportedMediaType;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ // Read and parse JSON body
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var product = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ // Validate required fields
+ var requiredFields = new string[] { "name", "price", "category" };
+ foreach (var field in requiredFields)
+ {
+ if (!product.Contains(field))
+ {
+ var error = $"{{\"error\":\"Missing required field: {field}\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(error));
+ return;
+ }
+ }
+
+ // Validate data types and ranges
+ if (!decimal.TryParse(product["price"].ToString(), out decimal price) || price <= 0)
+ {
+ var error = $"{{\"error\":\"Price must be a positive number\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(e.Context.Response, error);
+ return;
+ }
+
+ // Create product
+ var productId = CreateProduct(product);
+
+ var response = new ProductCreationResponse()
+ {
+ Message = "Product created successfully",
+ Id = productId,
+ Name = product["name"],
+ Price = price,
+ Category = product["category"],
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.Created;
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ catch (Exception ex)
+ {
+ var error = $"{{\"error\":\"Failed to create product: {ex.Message}\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+
+ private string CreateProduct(Hashtable product)
+ {
+ // Implement product creation logic
+ return Guid.NewGuid().ToString();
+ }
+}
+```
+
+### Form Data Handling
+
+```csharp
+public class UploadController
+{
+ [Route("api/upload")]
+ [Method("POST")]
+ public void UploadFile(WebServerEventArgs e)
+ {
+ try
+ {
+ var contentType = e.Context.Request.Headers?.GetValues("Content-Type")?[0];
+
+ if (contentType != null && contentType.StartsWith("multipart/form-data"))
+ {
+ // Handle multipart form data
+ var form = e.Context.Request.ReadForm();
+
+ var response = new UploadResponse()
+ {
+ Message = "Upload processed successfully",
+ Parameters = form.Parameters.Length,
+ Files = form.Files.Length,
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ else if (contentType == "application/x-www-form-urlencoded")
+ {
+ // Handle URL-encoded form data
+ var body = e.Context.Request.ReadBody();
+ var formData = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+
+ // Parse form data (implement parsing logic)
+ var fields = ParseFormData(formData);
+
+ var response = new UploadResponse()
+ {
+ Message = "Form data processed successfully",
+ Fields = fields,
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ else
+ {
+ var error = $"{{\"error\":\"Unsupported content type\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.UnsupportedMediaType;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+ catch (Exception ex)
+ {
+ var error = $"{{\"error\":\"Upload failed: {ex.Message}\"}}";
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(e.Context.Response, error);
+ }
+ }
+
+ private Hashtable ParseFormData(string formData)
+ {
+ var result = new Hashtable();
+ var pairs = formData.Split('&');
+
+ foreach (var pair in pairs)
+ {
+ var keyValue = pair.Split('=');
+ if (keyValue.Length == 2)
+ {
+ result[keyValue[0]] = keyValue[1];
+ }
+ }
+
+ return result;
+ }
+}
+```
+
+## Response Formats
+
+This section shows you the patterns for JSON, XML and CSV as returned types, the nanoFramework way!
+
+### JSON Responses
+
+```csharp
+public class ResponseController
+{
+ public class SensorDataResponse
+ {
+ public bool Success { get; set; }
+ public SensorData Data { get; set; }
+ public ResponseMeta Meta { get; set; }
+ }
+
+ public class SensorData
+ {
+ public double Temperature { get; set; }
+ public double Humidity { get; set; }
+ public double Pressure { get; set; }
+ public string Timestamp { get; set; }
+ }
+
+ public class ResponseMeta
+ {
+ public string Version { get; set; }
+ public string Source { get; set;
+ }
+
+ [Route("api/data/json")]
+ [Method("GET")]
+ public void GetJsonResponse(WebServerEventArgs e)
+ {
+ var data = new SensorDataResponse()
+ {
+ Success = true,
+ Data = new SensorData()
+ {
+ Temperature = 23.5,
+ Humidity = 65.2,
+ Pressure = 1013.25,
+ Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ },
+ Meta = new ResponseMeta()
+ {
+ Version = "1.0",
+ Source = "sensor_array_1"
+ }
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(data));
+ }
+}
+```
+
+### XML Responses
+
+```csharp
+public class XmlController
+{
+ [Route("api/data/xml")]
+ [Method("GET")]
+ public void GetXmlResponse(WebServerEventArgs e)
+ {
+ // As there is no official XML serializer/deserializer in nanoFramework, you'll have to create the XML manually.
+ // Note that in the real life, you will also remove all the career return and spaces to gain space.
+ // The date time shows how to best add it into an XML. You should do the same for the sensor data.
+ // Here, they are static just for the sample.
+ var xml = @"
+
+ true
+
+ 23.5
+ 65.2
+ 1013.25
+ " + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + @"
+
+";
+
+ e.Context.Response.ContentType = "application/xml";
+ WebServer.OutPutStream(e.Context.Response, xml);
+ }
+}
+```
+
+### CSV Responses
+
+```csharp
+public class CsvController
+{
+ [Route("api/data/csv")]
+ [Method("GET")]
+ public void GetCsvResponse(WebServerEventArgs e)
+ {
+ var csv = new StringBuilder();
+ csv.AppendLine("timestamp,temperature,humidity,pressure");
+ // This is a static example but you'll get those data from a sensor history for example.
+ csv.AppendLine($"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss},23.5,65.2,1013.25");
+ csv.AppendLine($"{DateTime.UtcNow.AddMinutes(-1):yyyy-MM-dd HH:mm:ss},23.3,65.5,1013.20");
+ csv.AppendLine($"{DateTime.UtcNow.AddMinutes(-2):yyyy-MM-dd HH:mm:ss},23.7,64.8,1013.30");
+
+ e.Context.Response.ContentType = "text/csv";
+ e.Context.Response.Headers.Add("Content-Disposition", "attachment; filename=sensor_data.csv");
+ WebServer.OutPutStream(e.Context.Response, csv.ToString());
+ }
+}
+```
+
+## Error Handling
+
+Here is an handy sample on how you can manage errors with a standard returned class.
+
+### Standardized Error Responses
+
+```csharp
+public class ErrorHandlingController
+{
+ public class ErrorDetail
+ {
+ public string Code { get; set; }
+ public string Message { get; set; }
+ public string Timestamp { get; set; }
+ public string Path { get; set;}
+ }
+
+ [Route("api/test/error")]
+ [Method("GET")]
+ public void TestError(WebServerEventArgs e)
+ {
+ var parameters = WebServer.DecodeParam(e.Context.Request.RawUrl);
+ var errorType = "500"; // Default
+
+ foreach (UrlParameter param in parameters)
+ {
+ if (param.Name == "type")
+ {
+ errorType = param.Value;
+ break;
+ }
+ }
+
+ switch (errorType)
+ {
+ case "400":
+ SendError(e.Context.Response, HttpStatusCode.BadRequest,
+ "BAD_REQUEST", "The request was malformed or invalid");
+ break;
+ case "401":
+ SendError(e.Context.Response, HttpStatusCode.Unauthorized,
+ "UNAUTHORIZED", "Authentication is required");
+ break;
+ case "403":
+ SendError(e.Context.Response, HttpStatusCode.Forbidden,
+ "FORBIDDEN", "Access to this resource is forbidden");
+ break;
+ case "404":
+ SendError(e.Context.Response, HttpStatusCode.NotFound,
+ "NOT_FOUND", "The requested resource was not found");
+ break;
+ case "429":
+ SendError(e.Context.Response, (HttpStatusCode)429,
+ "RATE_LIMITED", "Too many requests, please try again later");
+ break;
+ default:
+ SendError(e.Context.Response, HttpStatusCode.InternalServerError,
+ "INTERNAL_ERROR", "An unexpected error occurred");
+ break;
+ }
+ }
+
+ private void SendError(HttpListenerResponse response, HttpStatusCode statusCode,
+ string errorCode, string message)
+ {
+ var error = new ErrorDetail()
+ {
+ Code = errorCode,
+ Message = message,
+ Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
+ Path = response.StatusDescription
+ };
+
+ response.ContentType = "application/json";
+ response.StatusCode = (int)statusCode;
+ WebServer.OutPutStream(response, JsonConvert.SerializeObject(error));
+ }
+}
+```
+
+### Global Error Handler
+
+```csharp
+public class GlobalErrorHandler
+{
+ public static void HandleError(WebServerEventArgs e, Exception ex)
+ {
+ var error = new ErrorDetail()
+ {
+ Code = "INTERNAL_ERROR",
+ Message = "An unexpected error occurred",
+ Details = ex.Message,
+ Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(error));
+ }
+}
+```
+
+## Content Types
+
+Some application may need to support different content types. Here is a pattern to return what's needed and serialize it.
+
+### Content Negotiation
+
+```csharp
+public class ContentNegotiationController
+{
+ public class SensorReading
+ {
+ public double Temperature { get; set; }
+ public double Humidity { get; set; }
+ public DateTime Timestamp { get; set; }
+ }
+
+ [Route("api/data")]
+ [Method("GET")]
+ public void GetData(WebServerEventArgs e)
+ {
+ var acceptHeader = e.Context.Request.Headers?.GetValues("Accept")?[0] ?? "application/json";
+
+ var data = new SensorReading()
+ {
+ Temperature = 23.5,
+ Humidity = 65.2,
+ Timestamp = DateTime.UtcNow
+ };
+
+ if (acceptHeader.Contains("application/xml"))
+ {
+ // In real life, you'll remove the spaces and career return to gain space
+ // XML serialization needs to be done manually as there is no official nanoFramework nuget
+ var xml = $@"
+
+ {data.temperature}
+ {data.humidity}
+ {data.timestamp:yyyy-MM-ddTHH:mm:ssZ}
+";
+
+ e.Context.Response.ContentType = "application/xml";
+ WebServer.OutPutStream(e.Context.Response, xml);
+ }
+ else if (acceptHeader.Contains("text/plain"))
+ {
+ var text = $"Temperature: {data.temperature}°C\nHumidity: {data.humidity}%\nTimestamp: {data.timestamp}";
+
+ e.Context.Response.ContentType = "text/plain";
+ WebServer.OutPutStream(e.Context.Response, text);
+ }
+ else
+ {
+ // Default to JSON
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(data));
+ }
+ }
+}
+```
+
+## Authentication
+
+For detailed authentication, see the [specific documentation](./authentication.md). This section represents a summary.
+
+### API Key Authentication
+
+This samples show ho
+
+```csharp
+[Authentication("ApiKey")]
+public class SecureApiController
+{
+ [Route("api/secure/data")]
+ [Method("GET")]
+ public void GetSecureData(WebServerEventArgs e)
+ {
+ var secureData = new SecureData()
+ {
+ SensitiveInfo = "This is protected data",
+ AccessLevel = "admin",
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(secureData));
+ }
+
+ [Route("api/secure/config")]
+ [Method("POST")]
+ [Authentication("ApiKey:special-admin-key")]
+ public void UpdateConfig(WebServerEventArgs e)
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+
+ // Process configuration update
+ var response = new ConfigResponse()
+ {
+ Message = "Configuration updated successfully",
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+}
+```
+
+## Advanced Examples
+
+### Resource CRUD API
+
+Complete CRUD (Create, Read, Update, Delete) API for managing IoT devices:
+
+```csharp
+public class IoTDeviceController
+{
+ private static Hashtable _devices = new Hashtable();
+
+ public class DeviceResponse
+ {
+ public int Total { get; set; }
+ public object[] Devices { get; set; }
+ }
+
+ public class Device
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public string Status { get; set; }
+ public string CreatedAt { get; set; }
+ public string LastSeen { get; set; }
+ }
+
+ public class SensorDataResponse
+ {
+ public string DeviceId { get; set; }
+ public string SensorId { get; set; }
+ public double Value { get; set; }
+ public string Unit { get; set; }
+ public DateTime Timestamp { get; set; }
+ }
+
+ public class SensorConfigResponse
+ {
+ public string DeviceId { get; set; }
+ public string SensorId { get; set; }
+ public string Message { get; set; }
+ public DateTime Timestamp { get; set; }
+ }
+
+ public class DeleteResponse
+ {
+ public string Message { get; set; }
+ public string Timestamp { get; set; }
+ }
+
+ // GET /api/devices - List all devices
+ [Route("api/devices")]
+ [Method("GET")]
+ public void GetDevices(WebServerEventArgs e)
+ {
+ var devices = new object[_devices.Count];
+ int index = 0;
+
+ foreach (DictionaryEntry entry in _devices)
+ {
+ devices[index++] = entry.Value;
+ }
+
+ var response = new DeviceResponse()
+ {
+ Total = _devices.Count,
+ Devices = devices
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+
+ // GET /api/devices/{id} - Get specific device
+ [Route("api/devices/{id}")]
+ [Method("GET")]
+ public void GetDevice(WebServerEventArgs e)
+ {
+ string deviceId = e.GetRouteParameter("id");
+
+ if (_devices.Contains(deviceId))
+ {
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(_devices[deviceId]));
+ }
+ else
+ {
+ SendNotFound(e.Context.Response, $"Device {deviceId} not found");
+ }
+ }
+
+ // GET /api/devices/{deviceId}/sensors/{sensorId} - Get specific sensor data
+ [Route("api/devices/{deviceId}/sensors/{sensorId}")]
+ [Method("GET")]
+ public void GetDeviceSensor(WebServerEventArgs e)
+ {
+ string deviceId = e.GetRouteParameter("deviceId");
+ string sensorId = e.GetRouteParameter("sensorId");
+
+ if (_devices.Contains(deviceId))
+ {
+ var sensorData = GetSensorData(deviceId, sensorId);
+ if (sensorData != null)
+ {
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(sensorData));
+ }
+ else
+ {
+ SendNotFound(e.Context.Response, $"Sensor {sensorId} not found on device {deviceId}");
+ }
+ }
+ else
+ {
+ SendNotFound(e.Context.Response, $"Device {deviceId} not found");
+ }
+ }
+
+ // POST /api/devices - Create new device
+ [Route("api/devices")]
+ [Method("POST")]
+ public void CreateDevice(WebServerEventArgs e)
+ {
+ try
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var deviceData = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ // Validate required fields
+ if (!deviceData.Contains("name") || !deviceData.Contains("type"))
+ {
+ SendBadRequest(e.Context.Response, "Missing required fields: name, type");
+ return;
+ }
+
+ var deviceId = Guid.NewGuid().ToString();
+ var device = new Device()
+ {
+ Id = deviceId,
+ Name = deviceData["name"].ToString(),
+ Type = deviceData["type"].ToString(),
+ Status = "offline",
+ CreatedAt = DateTime.UtcNow.ToString(),
+ LastSeen = DateTime.UtcNow.ToString()
+ };
+
+ _devices[deviceId] = device;
+
+ e.Context.Response.ContentType = "application/json";
+ e.Context.Response.StatusCode = (int)HttpStatusCode.Created;
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(device));
+ }
+ catch (Exception ex)
+ {
+ SendInternalError(e.Context.Response, ex.Message);
+ }
+ }
+
+ // PUT /api/devices/{id} - Update device
+ [Route("api/devices/{id}")]
+ [Method("PUT")]
+ public void UpdateDevice(WebServerEventArgs e)
+ {
+ string deviceId = e.GetRouteParameter("id");
+
+ if (!_devices.Contains(deviceId))
+ {
+ SendNotFound(e.Context.Response, $"Device {deviceId} not found");
+ return;
+ }
+
+ try
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var updateData = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ var currentDevice = _devices[deviceId] as Hashtable;
+
+ // Update allowed fields
+ if (updateData.Contains("name"))
+ {
+ currentDevice["name"] = updateData["name"];
+ }
+ if (updateData.Contains("status"))
+ {
+ currentDevice["status"] = updateData["status"];
+ }
+ if (updateData.Contains("type"))
+ {
+ currentDevice["type"] = updateData["type"];
+ }
+
+ currentDevice["lastSeen"] = DateTime.UtcNow.ToString();
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(currentDevice));
+ }
+ catch (Exception ex)
+ {
+ SendInternalError(e.Context.Response, ex.Message);
+ }
+ }
+
+ // PUT /api/devices/{deviceId}/sensors/{sensorId} - Update sensor configuration
+ [Route("api/devices/{deviceId}/sensors/{sensorId}")]
+ [Method("PUT")]
+ public void UpdateDeviceSensor(WebServerEventArgs e)
+ {
+ string deviceId = e.GetRouteParameter("deviceId");
+ string sensorId = e.GetRouteParameter("sensorId");
+
+ if (!_devices.Contains(deviceId))
+ {
+ SendNotFound(e.Context.Response, $"Device {deviceId} not found");
+ return;
+ }
+
+ try
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var sensorUpdate = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ var result = UpdateSensorConfiguration(deviceId, sensorId, sensorUpdate);
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(result));
+ }
+ catch (Exception ex)
+ {
+ SendInternalError(e.Context.Response, ex.Message);
+ }
+ }
+
+ // DELETE /api/devices/{id} - Delete device
+ [Route("api/devices/{id}")]
+ [Method("DELETE")]
+ public void DeleteDevice(WebServerEventArgs e)
+ {
+ string deviceId = e.GetRouteParameter("id");
+
+ if (_devices.Contains(deviceId))
+ {
+ _devices.Remove(deviceId);
+
+ var response = new DeleteResponse()
+ {
+ Message = $"Device {deviceId} deleted successfully",
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ else
+ {
+ SendNotFound(e.Context.Response, $"Device {deviceId} not found");
+ }
+ }
+
+ // Helper methods
+ private SensorDataResponse GetSensorData(string deviceId, string sensorId)
+ {
+ // Implement sensor data retrieval logic
+ return new SensorDataResponse()
+ {
+ DeviceId = deviceId,
+ SensorId = sensorId,
+ Value = 23.5,
+ Unit = "°C",
+ Timestamp = DateTime.UtcNow
+ };
+ }
+
+ private SensorConfigResponse UpdateSensorConfiguration(string deviceId, string sensorId, Hashtable config)
+ {
+ // Implement sensor configuration update logic
+ return new SensorConfigResponse()
+ {
+ DeviceId = deviceId,
+ SensorId = sensorId,
+ Message = "Sensor configuration updated",
+ Timestamp = DateTime.UtcNow
+ };
+ }
+
+ private void SendBadRequest(HttpListenerResponse response, string message)
+ {
+ var error = $"{{\"error\":\"{message}\"}}";
+ response.ContentType = "application/json";
+ response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(response, error);
+ }
+
+ private void SendNotFound(HttpListenerResponse response, string message)
+ {
+ var error = $"{{\"error\":\"{message}\"}}";
+ response.ContentType = "application/json";
+ response.StatusCode = (int)HttpStatusCode.NotFound;
+ WebServer.OutPutStream(response, error);
+ }
+
+ private void SendInternalError(HttpListenerResponse response, string message)
+ {
+ var error = $"{{\"error\":\"Internal server error: {message}\"}}";
+ response.ContentType = "application/json";
+ response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(response, error);
+ }
+}
+```
+
+### Batch Operations API
+
+```csharp
+public class BatchController
+{
+ [Route("api/batch/sensors")]
+ [Method("POST")]
+ public void BatchUpdateSensors(WebServerEventArgs e)
+ {
+ try
+ {
+ var body = e.Context.Request.ReadBody();
+ var json = System.Text.Encoding.UTF8.GetString(body, 0, body.Length);
+ var batchRequest = JsonConvert.DeserializeObject(json, typeof(Hashtable)) as Hashtable;
+
+ if (!batchRequest.Contains("operations"))
+ {
+ SendBadRequest(e.Context.Response, "Missing 'operations' array");
+ return;
+ }
+
+ var operations = batchRequest["operations"] as ArrayList;
+ var results = new ArrayList();
+
+ foreach (Hashtable operation in operations)
+ {
+ var result = ProcessOperation(operation);
+ results.Add(result);
+ }
+
+ var response = new BatchResponse()
+ {
+ Message = "Batch operation completed",
+ TotalOperations = operations.Count,
+ Results = results.ToArray(),
+ Timestamp = DateTime.UtcNow.ToString()
+ };
+
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, JsonConvert.SerializeObject(response));
+ }
+ catch (Exception ex)
+ {
+ SendInternalError(e.Context.Response, ex.Message);
+ }
+ }
+
+ private object ProcessOperation(Hashtable operation)
+ {
+ try
+ {
+ var sensorId = operation["sensorId"].ToString();
+ var action = operation["action"].ToString();
+ var value = operation.Contains("value") ? operation["value"] : null;
+
+ // Process the operation based on action type
+ switch (action.ToLower())
+ {
+ case "read":
+ return new Sensor() { SensorId = sensorId, Action = action, Value = ReadSensor(sensorId), Success = true };
+ case "write":
+ WriteSensor(sensorId, value);
+ return new Sensor() { SensorId = sensorId, Action = action, Value = value, Success = true };
+ case "reset":
+ ResetSensor(sensorId);
+ return new Sensor() { SensorId = sensorId, Action = action, Success = true };
+ default:
+ return new Sensor() { SensorId = sensorId, Action = action, Success = false, Error = "Unknown action" };
+ }
+ }
+ catch (Exception ex)
+ {
+ return new Sensor() {
+ SensorId = operation.Contains("sensorId") ? operation["sensorId"] : "unknown",
+ Action = operation.Contains("action") ? operation["action"] : "unknown",
+ Success = false,
+ Error = ex.Message
+ };
+ }
+ }
+
+ private object ReadSensor(string sensorId)
+ {
+ // Implement sensor reading logic
+ return new Random().NextDouble() * 100;
+ }
+
+ private void WriteSensor(string sensorId, object value)
+ {
+ // Implement sensor writing logic
+ }
+
+ private void ResetSensor(string sensorId)
+ {
+ // Implement sensor reset logic
+ }
+
+ private void SendBadRequest(HttpListenerResponse response, string message)
+ {
+ var error = $"{{\"error\":\"{message}\"}}";
+ response.ContentType = "application/json";
+ response.StatusCode = (int)HttpStatusCode.BadRequest;
+ WebServer.OutPutStream(response, error);
+ }
+
+ private void SendInternalError(HttpListenerResponse response, string message)
+ {
+ var error = $"{{\"error\":\"Internal server error: {message}\"}}";
+ response.ContentType = "application/json";
+ response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ WebServer.OutPutStream(response, error);
+ }
+}
+```
+
+## Best Practices
+
+### 1. Consistent Response Format
+
+Use a standardized response format across all endpoints:
+
+```csharp
+public class StandardResponse
+{
+ public bool Success { get; set; }
+ public object Data { get; set; }
+ public string Message { get; set; }
+ public string Timestamp { get; set; }
+ public object Meta { get; set; }
+}
+
+public static class ResponseHelper
+{
+ public static void SendSuccessResponse(HttpListenerResponse response, object data, string message = null)
+ {
+ var standardResponse = new StandardResponse
+ {
+ Success = true,
+ Data = data,
+ Message = message,
+ Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ };
+
+ response.ContentType = "application/json";
+ WebServer.OutPutStream(response, JsonConvert.SerializeObject(standardResponse));
+ }
+
+ public static void SendErrorResponse(HttpListenerResponse response, HttpStatusCode statusCode, string message)
+ {
+ var standardResponse = new StandardResponse
+ {
+ Success = false,
+ Message = message,
+ Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ };
+
+ response.ContentType = "application/json";
+ response.StatusCode = (int)statusCode;
+ WebServer.OutPutStream(response, JsonConvert.SerializeObject(standardResponse));
+ }
+}
+```
+
+### 2. Input Validation
+
+```csharp
+public static class ValidationHelper
+{
+ public static bool ValidateRequired(Hashtable data, string[] requiredFields, out string missingField)
+ {
+ foreach (string field in requiredFields)
+ {
+ if (!data.Contains(field) || data[field] == null || string.IsNullOrEmpty(data[field].ToString()))
+ {
+ missingField = field;
+ return false;
+ }
+ }
+ missingField = null;
+ return true;
+ }
+
+ public static bool ValidateRange(object value, double min, double max)
+ {
+ if (double.TryParse(value.ToString(), out double numValue))
+ {
+ return numValue >= min && numValue <= max;
+ }
+ return false;
+ }
+
+ public static bool ValidateEmail(string email)
+ {
+ return !string.IsNullOrEmpty(email) && email.Contains("@") && email.Contains(".");
+ }
+}
+```
+
+### 3. Rate Limiting
+
+```csharp
+public class RateLimiter
+{
+ private static Hashtable _requestCounts = new Hashtable();
+ private static readonly int MaxRequests = 100;
+ private static readonly TimeSpan WindowSize = TimeSpan.FromMinutes(1);
+
+ public static bool IsRateLimited(string clientId)
+ {
+ var now = DateTime.UtcNow;
+ var key = $"{clientId}_{now:yyyyMMddHHmm}";
+
+ if (!_requestCounts.Contains(key))
+ {
+ _requestCounts[key] = 1;
+ CleanupOldEntries(now);
+ return false;
+ }
+
+ var count = (int)_requestCounts[key];
+ if (count >= MaxRequests)
+ {
+ return true;
+ }
+
+ _requestCounts[key] = count + 1;
+ return false;
+ }
+
+ private static void CleanupOldEntries(DateTime now)
+ {
+ var keysToRemove = new ArrayList();
+
+ foreach (DictionaryEntry entry in _requestCounts)
+ {
+ var key = entry.Key.ToString();
+ var timestamp = key.Substring(key.LastIndexOf('_') + 1);
+
+ if (DateTime.TryParseExact(timestamp, "yyyyMMddHHmm", null, DateTimeStyles.None, out DateTime entryTime))
+ {
+ if (now - entryTime > WindowSize)
+ {
+ keysToRemove.Add(key);
+ }
+ }
+ }
+
+ foreach (string key in keysToRemove)
+ {
+ _requestCounts.Remove(key);
+ }
+ }
+}
+```
+
+## Testing
+
+### Using HTTP Tools
+
+Test your REST API with curl, Postman, or VS Code REST Client:
+
+```http
+### Get device status
+GET http://192.168.1.100/api/status
+Accept: application/json
+
+### Get all devices
+GET http://192.168.1.100/api/devices
+Accept: application/json
+
+### Get specific device by ID
+GET http://192.168.1.100/api/devices/123e4567-e89b-12d3-a456-426614174000
+Accept: application/json
+
+### Get specific sensor data
+GET http://192.168.1.100/api/devices/esp32-001/sensors/temperature
+Accept: application/json
+
+### Create a new device
+POST http://192.168.1.100/api/devices
+Content-Type: application/json
+
+{
+ "name": "Temperature Sensor 01",
+ "type": "temperature",
+ "location": "Living Room"
+}
+
+### Update device by ID
+PUT http://192.168.1.100/api/devices/123e4567-e89b-12d3-a456-426614174000
+Content-Type: application/json
+
+{
+ "name": "Updated Temperature Sensor",
+ "status": "online"
+}
+
+### Update sensor configuration
+PUT http://192.168.1.100/api/devices/esp32-001/sensors/temperature
+Content-Type: application/json
+
+{
+ "sampleRate": 5000,
+ "threshold": 25.0
+}
+
+### Delete specific device
+DELETE http://192.168.1.100/api/devices/123e4567-e89b-12d3-a456-426614174000
+
+### Search with query parameters
+GET http://192.168.1.100/api/search?q=temperature&limit=10&sort=date
+Accept: application/json
+
+### Test error handling
+GET http://192.168.1.100/api/test/error?type=404
+```
+
+## Related Resources
+
+- [Controllers and Routing](./controllers-routing.md) - Route configuration and controller setup
+- [Authentication](./authentication.md) - Securing your APIs
+- [File System Support](./file-system.md) - Serving static files
+- [Event-Driven Programming](./event-driven.md) - Alternative to controller-based APIs
+- [Examples and Samples](./examples.md) - More complete examples
+
+The REST API support in nanoFramework WebServer provides a solid foundation for building modern, scalable APIs on embedded devices, enabling seamless integration with web applications, mobile apps, and other IoT systems.
diff --git a/doc/samples.md b/doc/samples.md
new file mode 100644
index 0000000..72a38b6
--- /dev/null
+++ b/doc/samples.md
@@ -0,0 +1,749 @@
+# Event-Driven Programming
+
+The nanoFramework WebServer provides a powerful event-driven architecture that allows developers to handle HTTP requests dynamically and monitor server status changes. This approach offers flexibility for scenarios where attribute-based routing isn't sufficient or when you need fine-grained control over request processing.
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [CommandReceived Event](#commandreceived-event)
+3. [WebServerStatusChanged Event](#webserverstatuschanged-event)
+4. [WebServerEventArgs](#webservereventargs)
+5. [Basic Event Handling Examples](#basic-event-handling-examples)
+6. [Advanced Scenarios](#advanced-scenarios)
+7. [Event Handling Best Practices](#event-handling-best-practices)
+8. [Error Handling](#error-handling)
+9. [Performance Considerations](#performance-considerations)
+
+## Overview
+
+The nanoFramework WebServer supports two primary events:
+
+- **CommandReceived**: Triggered when an HTTP request is received that doesn't match any registered controller routes
+- **WebServerStatusChanged**: Triggered when the server status changes (starting, running, stopped)
+
+Event-driven programming is particularly useful for:
+- Dynamic request handling without predefined routes
+- Custom authentication and authorization logic
+- Request logging and monitoring
+- Server lifecycle management
+- Fallback handling for unmatched routes
+
+## CommandReceived Event
+
+The `CommandReceived` event is the primary mechanism for handling HTTP requests in an event-driven manner.
+
+### Event Signature
+
+```csharp
+public delegate void GetRequestHandler(object obj, WebServerEventArgs e);
+public event GetRequestHandler CommandReceived;
+```
+
+### Basic Usage
+
+```csharp
+using System;
+using System.Diagnostics;
+using System.Net;
+using nanoFramework.WebServer;
+
+// Create WebServer instance
+var server = new WebServer(80, HttpProtocol.Http);
+
+// Subscribe to the event
+server.CommandReceived += ServerCommandReceived;
+
+// Start the server
+server.Start();
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+ var method = e.Context.Request.HttpMethod;
+
+ Debug.WriteLine($"Command received: {url}, Method: {method}");
+
+ if (url.ToLower() == "/hello")
+ {
+ WebServer.OutPutStream(e.Context.Response, "Hello from nanoFramework!");
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+### Parameter Handling
+
+Extract and process URL parameters in event handlers:
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.ToLower().IndexOf("/param.htm") == 0)
+ {
+ // Extract parameters from URL
+ var parameters = WebServer.DecodeParam(url);
+
+ string response = "Parameters";
+ response += "URL Parameters:
";
+
+ if (parameters != null)
+ {
+ foreach (var param in parameters)
+ {
+ response += $"Parameter: {param.Name} = {param.Value}
";
+ }
+ }
+
+ response += "";
+ WebServer.OutPutStream(e.Context.Response, response);
+ }
+}
+```
+
+### File Serving Example
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.IndexOf("/download/") == 0)
+ {
+ string fileName = url.Substring(10); // Remove "/download/"
+ string filePath = $"I:\\{fileName}";
+
+ if (File.Exists(filePath))
+ {
+ WebServer.SendFileOverHTTP(e.Context.Response, filePath);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+ else if (url.ToLower() == "/createfile")
+ {
+ // Create a test file
+ File.WriteAllText("I:\\test.txt", "This is a dynamically created file");
+ WebServer.OutPutStream(e.Context.Response, "File created successfully");
+ }
+}
+```
+
+## WebServerStatusChanged Event
+
+Monitor server status changes to implement robust server management.
+
+### Event Signature
+
+```csharp
+public delegate void WebServerStatusHandler(object obj, WebServerStatusEventArgs e);
+public event WebServerStatusHandler WebServerStatusChanged;
+```
+
+### Status Values
+
+The server can be in one of the following states:
+
+```csharp
+public enum WebServerStatus
+{
+ Stopped,
+ Running
+}
+```
+
+### Basic Status Monitoring
+
+```csharp
+var server = new WebServer(80, HttpProtocol.Http);
+
+// Subscribe to status changes
+server.WebServerStatusChanged += OnWebServerStatusChanged;
+
+server.Start();
+
+private static void OnWebServerStatusChanged(object obj, WebServerStatusEventArgs e)
+{
+ Debug.WriteLine($"Server status changed to: {e.Status}");
+
+ if (e.Status == WebServerStatus.Running)
+ {
+ Debug.WriteLine("Server is now accepting requests");
+ // Initialize additional services
+ }
+ else if (e.Status == WebServerStatus.Stopped)
+ {
+ Debug.WriteLine("Server has stopped");
+ // Cleanup or restart logic
+ }
+}
+```
+
+### Server Recovery Pattern
+
+```csharp
+private static void OnWebServerStatusChanged(object obj, WebServerStatusEventArgs e)
+{
+ if (e.Status == WebServerStatus.Stopped)
+ {
+ Debug.WriteLine("Server stopped unexpectedly. Attempting restart...");
+
+ // Wait a moment before restart
+ Thread.Sleep(5000);
+
+ try
+ {
+ var server = (WebServer)obj;
+ if (server.Start())
+ {
+ Debug.WriteLine("Server successfully restarted");
+ }
+ else
+ {
+ Debug.WriteLine("Failed to restart server");
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error restarting server: {ex.Message}");
+ }
+ }
+}
+```
+
+## WebServerEventArgs
+
+The `WebServerEventArgs` class provides access to the HTTP context and all request/response information.
+
+### Properties
+
+```csharp
+public class WebServerEventArgs
+{
+ public HttpListenerContext Context { get; protected set; }
+}
+```
+
+### Accessing Request Information
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ // Request properties
+ string url = request.RawUrl;
+ string method = request.HttpMethod;
+ string contentType = request.ContentType;
+ var headers = request.Headers;
+ var inputStream = request.InputStream;
+
+ // Process request data
+ if (method == "POST" && request.InputStream.Length > 0)
+ {
+ byte[] buffer = new byte[request.InputStream.Length];
+ request.InputStream.Read(buffer, 0, buffer.Length);
+ string postData = System.Text.Encoding.UTF8.GetString(buffer, 0, buffer.Length);
+
+ Debug.WriteLine($"POST data: {postData}");
+ }
+
+ // Send response
+ WebServer.OutPutStream(response, "Request processed successfully");
+}
+```
+
+## Basic Event Handling Examples
+
+### Simple Text Response
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ switch (url.ToLower())
+ {
+ case "/":
+ WebServer.OutPutStream(e.Context.Response, "Welcome to nanoFramework WebServer!");
+ break;
+
+ case "/time":
+ WebServer.OutPutStream(e.Context.Response, $"Current time: {DateTime.UtcNow}");
+ break;
+
+ case "/info":
+ var info = $"Server running on nanoFramework\nUptime: {Environment.TickCount}ms";
+ WebServer.OutPutStream(e.Context.Response, info);
+ break;
+
+ default:
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ break;
+ }
+}
+```
+
+### HTML Response with Dynamic Content
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.ToLower() == "/dashboard")
+ {
+ string html = $@"
+
+
+ nanoFramework Dashboard
+
+
+
+ System Dashboard
+ Current Time: {DateTime.UtcNow}
+ Uptime: {Environment.TickCount} ms
+ Free Memory: {System.GC.GetTotalMemory(false)} bytes
+ Home | Info
+
+ ";
+
+ WebServer.OutPutStream(e.Context.Response, html);
+ }
+}
+```
+
+### JSON API Response
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url.ToLower().IndexOf("/api/") == 0)
+ {
+ e.Context.Response.ContentType = "application/json";
+
+ if (url.ToLower() == "/api/status")
+ {
+ // Example of a manual json serialization. You can also use the nanoFramework.Json nuget.
+ string json = $@"{{
+ ""status"": ""running"",
+ ""timestamp"": ""{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}"",
+ ""uptime"": {Environment.TickCount},
+ ""memory"": {System.GC.GetTotalMemory(false)}
+ }}";
+
+ WebServer.OutPutStream(e.Context.Response, json);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+ }
+}
+```
+
+## Advanced Scenarios
+
+### Request Logging and Analytics
+
+```csharp
+private static readonly ArrayList RequestLog = new ArrayList();
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+
+ // Log request details
+ var logEntry = new LogEntry()
+ {
+ Timestamp = DateTime.UtcNow,
+ Method = request.HttpMethod,
+ Url = request.RawUrl,
+ UserAgent = request.Headers["User-Agent"],
+ RemoteEndPoint = request.RemoteEndPoint?.ToString()
+ };
+
+ RequestLog.Add(logEntry);
+ Debug.WriteLine($"Request logged: {request.HttpMethod} {request.RawUrl}");
+
+ // Limit log size
+ if (RequestLog.Count > 100)
+ {
+ RequestLog.RemoveAt(0);
+ }
+
+ // Handle request normally
+ HandleRequest(e);
+}
+
+private static void HandleRequest(WebServerEventArgs e)
+{
+ // Your normal request handling logic
+ var url = e.Context.Request.RawUrl;
+
+ if (url == "/logs")
+ {
+ // Return request logs
+ string response = "Recent Requests:\n";
+ foreach (var entry in RequestLog)
+ {
+ response += $"{entry}\n";
+ }
+ WebServer.OutPutStream(e.Context.Response, response);
+ }
+ else
+ {
+ // Handle other requests
+ WebServer.OutPutStream(e.Context.Response, "Request processed");
+ }
+}
+```
+
+### Custom Authentication
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var url = request.RawUrl;
+
+ // Check if route requires authentication
+ if (RequiresAuth(url))
+ {
+ if (!IsAuthenticated(request))
+ {
+ e.Context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Secure Area\"");
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.Unauthorized);
+ return;
+ }
+ }
+
+ // Process authenticated request
+ HandleAuthenticatedRequest(e);
+}
+
+private static bool RequiresAuth(string url)
+{
+ return url.StartsWith("/admin/") || url.StartsWith("/secure/");
+}
+
+private static bool IsAuthenticated(HttpListenerRequest request)
+{
+ var credentials = request.Credentials;
+ if (credentials == null) return false;
+
+ // Check credentials against your authentication system
+ return credentials.UserName == "admin" && credentials.Password == "password";
+}
+```
+
+### Content Type Handling
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ // Handle different content types
+ switch (request.ContentType?.ToLower())
+ {
+ case "application/json":
+ HandleJsonRequest(e);
+ break;
+
+ case "application/x-www-form-urlencoded":
+ HandleFormRequest(e);
+ break;
+
+ case "multipart/form-data":
+ HandleMultipartRequest(e);
+ break;
+
+ default:
+ HandleDefaultRequest(e);
+ break;
+ }
+}
+
+private static void HandleJsonRequest(WebServerEventArgs e)
+{
+ // Read JSON payload
+ var buffer = new byte[e.Context.Request.InputStream.Length];
+ e.Context.Request.InputStream.Read(buffer, 0, buffer.Length);
+ string jsonData = System.Text.Encoding.UTF8.GetString(buffer, 0, buffer.Length);
+
+ Debug.WriteLine($"Received JSON: {jsonData}");
+
+ // Process JSON and respond
+ e.Context.Response.ContentType = "application/json";
+ WebServer.OutPutStream(e.Context.Response, "{\"status\":\"success\"}");
+}
+```
+
+## Event Handling Best Practices
+
+### 1. Always Handle Exceptions
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ try
+ {
+ HandleRequest(e);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error handling request: {ex.Message}");
+
+ try
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.InternalServerError);
+ }
+ catch
+ {
+ // Context might be disposed, ignore
+ }
+ }
+}
+```
+
+### 2. Use Asynchronous Processing for Long Operations
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ if (url == "/longprocess")
+ {
+ // Start long operation in background thread
+ new Thread(() =>
+ {
+ try
+ {
+ ProcessLongRunningTask(e);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Background task error: {ex.Message}");
+ }
+ }).Start();
+
+ // Return immediate response
+ WebServer.OutPutStream(e.Context.Response, "Processing started");
+ }
+}
+```
+
+### 3. Validate Input Data
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+
+ // Validate HTTP method
+ if (request.HttpMethod != "GET" && request.HttpMethod != "POST")
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.MethodNotAllowed);
+ return;
+ }
+
+ // Validate URL format
+ if (string.IsNullOrEmpty(request.RawUrl))
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.BadRequest);
+ return;
+ }
+
+ // Continue with processing
+ HandleValidatedRequest(e);
+}
+```
+
+### 4. Implement Proper Resource Cleanup
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ FileStream fileStream = null;
+
+ try
+ {
+ var url = e.Context.Request.RawUrl;
+
+ if (url.StartsWith("/file/"))
+ {
+ string fileName = url.Substring(6);
+ fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
+
+ // Process file
+ WebServer.SendFileOverHTTP(e.Context.Response, fileName);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error: {ex.Message}");
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.InternalServerError);
+ }
+ finally
+ {
+ fileStream?.Dispose();
+ }
+}
+```
+
+## Error Handling
+
+### Graceful Error Recovery
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ try
+ {
+ ProcessRequest(e);
+ }
+ catch (OutOfMemoryException)
+ {
+ // Force garbage collection
+ System.GC.Collect();
+
+ response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ WebServer.OutPutStream(response, "Service temporarily unavailable - low memory");
+ }
+ catch (System.IO.IOException ioEx)
+ {
+ Debug.WriteLine($"IO Error: {ioEx.Message}");
+ WebServer.OutputHttpCode(response, HttpStatusCode.InternalServerError);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Unexpected error: {ex.Message}");
+ WebServer.OutputHttpCode(response, HttpStatusCode.InternalServerError);
+ }
+}
+```
+
+### Request Timeout Handling
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var timeoutTimer = new Timer(HandleTimeout, e.Context, 30000, Timeout.Infinite);
+
+ try
+ {
+ ProcessRequest(e);
+ timeoutTimer.Dispose();
+ }
+ catch
+ {
+ timeoutTimer.Dispose();
+ throw;
+ }
+}
+
+private static void HandleTimeout(object state)
+{
+ var context = (HttpListenerContext)state;
+
+ try
+ {
+ WebServer.OutputHttpCode(context.Response, HttpStatusCode.RequestTimeout);
+ }
+ catch
+ {
+ // Context might be disposed
+ }
+}
+```
+
+## Performance Considerations
+
+### 1. Minimize Allocations in Event Handlers
+
+```csharp
+// Reuse string builders and buffers
+private static readonly StringBuilder ResponseBuilder = new StringBuilder();
+private static readonly byte[] Buffer = new byte[1024];
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ ResponseBuilder.Clear();
+ ResponseBuilder.Append("Response data: ");
+ ResponseBuilder.Append(DateTime.UtcNow);
+
+ WebServer.OutPutStream(e.Context.Response, ResponseBuilder.ToString());
+}
+```
+
+### 2. Cache Frequently Used Data
+
+Note that cashing elements consumes memory< Be mindfull that you are on an embedded device.
+
+```csharp
+private static readonly Hashtable ResponseCache = new Hashtable();
+
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ // Check cache first
+ if (ResponseCache.Contains(url))
+ {
+ string cachedResponse = (string)ResponseCache[url];
+ WebServer.OutPutStream(e.Context.Response, cachedResponse);
+ return;
+ }
+
+ // Generate response
+ string response = GenerateResponse(url);
+
+ // Cache response (with size limit)
+ if (ResponseCache.Count < 50)
+ {
+ ResponseCache[url] = response;
+ }
+
+ WebServer.OutPutStream(e.Context.Response, response);
+}
+```
+
+### 3. Use Efficient String Operations
+
+```csharp
+private static void ServerCommandReceived(object source, WebServerEventArgs e)
+{
+ var url = e.Context.Request.RawUrl;
+
+ // Use IndexOf instead of StartsWith for better performance on nanoFramework
+ if (url.IndexOf("/api/") == 0)
+ {
+ HandleApiRequest(e);
+ }
+ else if (url.IndexOf("/static/") == 0)
+ {
+ HandleStaticContent(e);
+ }
+ else
+ {
+ WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.NotFound);
+ }
+}
+```
+
+The event-driven approach provides maximum flexibility for handling HTTP requests in nanoFramework applications. By combining the `CommandReceived` and `WebServerStatusChanged` events with proper error handling and performance considerations, you can build robust and responsive web applications that handle a wide variety of scenarios.
diff --git a/nanoFramework.WebServer.Mcp.nuspec b/nanoFramework.WebServer.Mcp.nuspec
index 6a4eb63..574a28a 100644
--- a/nanoFramework.WebServer.Mcp.nuspec
+++ b/nanoFramework.WebServer.Mcp.nuspec
@@ -23,7 +23,7 @@ This comes also with the nanoFramework WebServer. Allowing to create a REST API
-
+
diff --git a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj
index 603621a..373655d 100644
--- a/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj
+++ b/nanoFramework.WebServer.Mcp/nanoFramework.WebServer.Mcp.nfproj
@@ -46,7 +46,7 @@
..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll
- ..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll
+ ..\packages\nanoFramework.Json.2.2.203\lib\nanoFramework.Json.dll
..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll
diff --git a/nanoFramework.WebServer.Mcp/packages.config b/nanoFramework.WebServer.Mcp/packages.config
index dd917ed..b41cf42 100644
--- a/nanoFramework.WebServer.Mcp/packages.config
+++ b/nanoFramework.WebServer.Mcp/packages.config
@@ -1,7 +1,7 @@
-
+
diff --git a/nanoFramework.WebServer.Mcp/packages.lock.json b/nanoFramework.WebServer.Mcp/packages.lock.json
index 513c863..55840f0 100644
--- a/nanoFramework.WebServer.Mcp/packages.lock.json
+++ b/nanoFramework.WebServer.Mcp/packages.lock.json
@@ -10,9 +10,9 @@
},
"nanoFramework.Json": {
"type": "Direct",
- "requested": "[2.2.199, 2.2.199]",
- "resolved": "2.2.199",
- "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w=="
+ "requested": "[2.2.203, 2.2.203]",
+ "resolved": "2.2.203",
+ "contentHash": "IsbevoAqPill+t5sU6uLWW8/UgpwfmEUWrZvzwxAOWMnj+wY/68oHcp8N/eJqcGYsUHAyLKKR/LCg4BSgDcZbw=="
},
"nanoFramework.Runtime.Events": {
"type": "Direct",
diff --git a/tests/McpEndToEndTest/McpEndToEndTest.nfproj b/tests/McpEndToEndTest/McpEndToEndTest.nfproj
index 1d87e77..d3a69b4 100644
--- a/tests/McpEndToEndTest/McpEndToEndTest.nfproj
+++ b/tests/McpEndToEndTest/McpEndToEndTest.nfproj
@@ -28,7 +28,7 @@
..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll
- ..\..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll
+ ..\..\packages\nanoFramework.Json.2.2.203\lib\nanoFramework.Json.dll
..\..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll
diff --git a/tests/McpEndToEndTest/packages.config b/tests/McpEndToEndTest/packages.config
index dc88051..67b05e3 100644
--- a/tests/McpEndToEndTest/packages.config
+++ b/tests/McpEndToEndTest/packages.config
@@ -1,7 +1,7 @@
-
+
diff --git a/tests/McpEndToEndTest/packages.lock.json b/tests/McpEndToEndTest/packages.lock.json
index 23e1f39..24607ba 100644
--- a/tests/McpEndToEndTest/packages.lock.json
+++ b/tests/McpEndToEndTest/packages.lock.json
@@ -10,9 +10,9 @@
},
"nanoFramework.Json": {
"type": "Direct",
- "requested": "[2.2.199, 2.2.199]",
- "resolved": "2.2.199",
- "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w=="
+ "requested": "[2.2.203, 2.2.203]",
+ "resolved": "2.2.203",
+ "contentHash": "IsbevoAqPill+t5sU6uLWW8/UgpwfmEUWrZvzwxAOWMnj+wY/68oHcp8N/eJqcGYsUHAyLKKR/LCg4BSgDcZbw=="
},
"nanoFramework.Runtime.Events": {
"type": "Direct",
diff --git a/tests/nanoFramework.WebServer.Tests/nanoFramework.WebServer.Tests.nfproj b/tests/nanoFramework.WebServer.Tests/nanoFramework.WebServer.Tests.nfproj
index a680f7c..6d98968 100644
--- a/tests/nanoFramework.WebServer.Tests/nanoFramework.WebServer.Tests.nfproj
+++ b/tests/nanoFramework.WebServer.Tests/nanoFramework.WebServer.Tests.nfproj
@@ -37,8 +37,8 @@
..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll
-
- ..\..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll
+
+ ..\..\packages\nanoFramework.Json.2.2.203\lib\nanoFramework.Json.dll
..\..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll
diff --git a/tests/nanoFramework.WebServer.Tests/packages.config b/tests/nanoFramework.WebServer.Tests/packages.config
index ffa60e4..ca3592a 100644
--- a/tests/nanoFramework.WebServer.Tests/packages.config
+++ b/tests/nanoFramework.WebServer.Tests/packages.config
@@ -1,7 +1,7 @@
-
+
diff --git a/tests/nanoFramework.WebServer.Tests/packages.lock.json b/tests/nanoFramework.WebServer.Tests/packages.lock.json
index 95e67de..81ad0d1 100644
--- a/tests/nanoFramework.WebServer.Tests/packages.lock.json
+++ b/tests/nanoFramework.WebServer.Tests/packages.lock.json
@@ -10,9 +10,9 @@
},
"nanoFramework.Json": {
"type": "Direct",
- "requested": "[2.2.199, 2.2.199]",
- "resolved": "2.2.199",
- "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w=="
+ "requested": "[2.2.203, 2.2.203]",
+ "resolved": "2.2.203",
+ "contentHash": "IsbevoAqPill+t5sU6uLWW8/UgpwfmEUWrZvzwxAOWMnj+wY/68oHcp8N/eJqcGYsUHAyLKKR/LCg4BSgDcZbw=="
},
"nanoFramework.Runtime.Events": {
"type": "Direct",