Initial import of FsMcp project, ignore mcp.json, add mcp.example.json
This commit is contained in:
24
.gitignore
vendored
24
.gitignore
vendored
@@ -12,3 +12,27 @@
|
|||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# MCP local credentials/config
|
||||||
|
mcp.json
|
||||||
|
.mcp.json
|
||||||
|
.vscode/mcp.json
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
3
FsMcp.slnx
Normal file
3
FsMcp.slnx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="FsMcp/FsMcp.csproj" />
|
||||||
|
</Solution>
|
||||||
22
FsMcp/.mcp/server.json
Normal file
22
FsMcp/.mcp/server.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
||||||
|
"description": "<your description here>",
|
||||||
|
"name": "io.github.<your GitHub username here>/<your repo name>",
|
||||||
|
"version": "0.1.0-beta",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"registryType": "nuget",
|
||||||
|
"identifier": "<your package ID here>",
|
||||||
|
"version": "0.1.0-beta",
|
||||||
|
"transport": {
|
||||||
|
"type": "stdio"
|
||||||
|
},
|
||||||
|
"packageArguments": [],
|
||||||
|
"environmentVariables": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/<your GitHub username here>/<your repo name>",
|
||||||
|
"source": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
FsMcp/Endpoints.cs
Normal file
9
FsMcp/Endpoints.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace FsMcp;
|
||||||
|
|
||||||
|
public static class Endpoints
|
||||||
|
{
|
||||||
|
public static string UserLogin => $"{ApiBase}/api/login";
|
||||||
|
public static string UserCurrentDetails => $"{ApiBase}/api/users/current/details";
|
||||||
|
|
||||||
|
private static string ApiBase => "https://beta.foodsharing.de";
|
||||||
|
}
|
||||||
103
FsMcp/FoodsharingApiClient.cs
Normal file
103
FsMcp/FoodsharingApiClient.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace FsMcp;
|
||||||
|
|
||||||
|
internal sealed class FoodsharingApiClient
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _loginLock = new(1, 1);
|
||||||
|
|
||||||
|
public HttpClient HttpClient { get; } = new(new RequestThrottleHandler(TimeSpan.FromMilliseconds(200)));
|
||||||
|
|
||||||
|
public async Task EnsureLoginAsync()
|
||||||
|
{
|
||||||
|
if (HttpClient.DefaultRequestHeaders.Contains("X-CSRF-Token"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _loginLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (HttpClient.DefaultRequestHeaders.Contains("X-CSRF-Token"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string username = Environment.GetEnvironmentVariable("USERNAME") ?? throw new InvalidOperationException("USERNAME environment variable is not set.");
|
||||||
|
string password = Environment.GetEnvironmentVariable("PASSWORD") ?? throw new InvalidOperationException("PASSWORD environment variable is not set.");
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
email = username,
|
||||||
|
password,
|
||||||
|
remember_me = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var response = await HttpClient.PostAsJsonAsync(Endpoints.UserLogin, payload);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new InvalidOperationException($"Login failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.Headers.TryGetValues("Set-Cookie", out var setCookieValues))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Set-Cookie headers are missing from login response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string setCookieHeader = string.Join(";", setCookieValues);
|
||||||
|
string? csrfToken = setCookieHeader
|
||||||
|
.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(cookie => cookie.Trim())
|
||||||
|
.FirstOrDefault(cookie => cookie.StartsWith("FS_CSRF_TOKEN="))
|
||||||
|
?.Split('=', 2)[1];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(csrfToken))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("CSRF token not found in Set-Cookie header.");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpClient.DefaultRequestHeaders.Remove("X-CSRF-Token");
|
||||||
|
HttpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loginLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RequestThrottleHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly TimeSpan _minimumInterval;
|
||||||
|
private readonly SemaphoreSlim _requestGate = new(1, 1);
|
||||||
|
private DateTimeOffset _lastRequestStartedAt = DateTimeOffset.MinValue;
|
||||||
|
|
||||||
|
public RequestThrottleHandler(TimeSpan minimumInterval)
|
||||||
|
: base(new HttpClientHandler())
|
||||||
|
{
|
||||||
|
_minimumInterval = minimumInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _requestGate.WaitAsync(cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var waitFor = _lastRequestStartedAt + _minimumInterval - DateTimeOffset.UtcNow;
|
||||||
|
if (waitFor > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
await Task.Delay(waitFor, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastRequestStartedAt = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_requestGate.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
FsMcp/FsMcp.csproj
Normal file
40
FsMcp/FsMcp.csproj
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RuntimeIdentifiers>win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64</RuntimeIdentifiers>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|
||||||
|
<!-- Set up the NuGet package to be an MCP server -->
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<PackageType>McpServer</PackageType>
|
||||||
|
|
||||||
|
<!-- Set up the MCP server to be a self-contained application that does not rely on a shared framework -->
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSelfContained>true</PublishSelfContained>
|
||||||
|
|
||||||
|
<!-- Set up the MCP server to be a single file executable -->
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|
||||||
|
<!-- Set recommended package metadata -->
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageId>SampleMcpServer</PackageId>
|
||||||
|
<PackageVersion>0.1.0-beta</PackageVersion>
|
||||||
|
<PackageTags>AI; MCP; server; stdio</PackageTags>
|
||||||
|
<Description>An MCP server using the MCP C# SDK.</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Include additional files for browsing the MCP server. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include=".mcp\server.json" Pack="true" PackagePath="/.mcp/" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
|
<PackageReference Include="ModelContextProtocol" Version="0.7.0-preview.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
19
FsMcp/Program.cs
Normal file
19
FsMcp/Program.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using FsMcp;
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
// Configure all logs to go to stderr (stdout is used for the MCP protocol messages).
|
||||||
|
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
|
||||||
|
builder.Services.AddSingleton<FoodsharingApiClient>();
|
||||||
|
|
||||||
|
// Add the MCP services: the transport to use (stdio) and the tools to register.
|
||||||
|
builder.Services
|
||||||
|
.AddMcpServer()
|
||||||
|
.WithStdioServerTransport()
|
||||||
|
.WithTools<RandomNumberTools>()
|
||||||
|
.WithTools<CurrentUserTools>();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
115
FsMcp/README.md
Normal file
115
FsMcp/README.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# MCP Server
|
||||||
|
|
||||||
|
This README was created using the C# MCP server project template.
|
||||||
|
It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package.
|
||||||
|
|
||||||
|
The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine.
|
||||||
|
However, since it is self-contained, it must be built for each target platform separately.
|
||||||
|
By default, the template is configured to build for:
|
||||||
|
* `win-x64`
|
||||||
|
* `win-arm64`
|
||||||
|
* `osx-arm64`
|
||||||
|
* `linux-x64`
|
||||||
|
* `linux-arm64`
|
||||||
|
* `linux-musl-x64`
|
||||||
|
|
||||||
|
If your users require more platforms to be supported, update the list of runtime identifiers in the project's `<RuntimeIdentifiers />` element.
|
||||||
|
|
||||||
|
See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide.
|
||||||
|
|
||||||
|
Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](http://aka.ms/dotnet-mcp-template-survey).
|
||||||
|
|
||||||
|
## Checklist before publishing to NuGet.org
|
||||||
|
|
||||||
|
- Test the MCP server locally using the steps below.
|
||||||
|
- Update the package metadata in the .csproj file, in particular the `<PackageId>`.
|
||||||
|
- Update `.mcp/server.json` to declare your MCP server's inputs.
|
||||||
|
- See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details.
|
||||||
|
- Pack the project using `dotnet pack`.
|
||||||
|
|
||||||
|
The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package).
|
||||||
|
|
||||||
|
## Developing locally
|
||||||
|
|
||||||
|
To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"FsMcp": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"<PATH TO PROJECT DIRECTORY>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers:
|
||||||
|
|
||||||
|
- [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
|
||||||
|
- [Use MCP servers in Visual Studio (Preview)](https://learn.microsoft.com/visualstudio/ide/mcp-servers)
|
||||||
|
|
||||||
|
## Testing the MCP Server
|
||||||
|
|
||||||
|
Once configured, you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `FsMcp` MCP server and show you the results.
|
||||||
|
|
||||||
|
### Current user profile tool
|
||||||
|
|
||||||
|
This server also exposes `get_current_user_details`.
|
||||||
|
|
||||||
|
- Purpose: Returns full details for the currently authenticated user from `GET /api/users/current/details`.
|
||||||
|
- Auth: Uses `USERNAME` and `PASSWORD` environment variables to log in and then sends the `X-CSRF-Token` header.
|
||||||
|
- Output: Structured profile object including:
|
||||||
|
- identity and account status (`id`, `foodsaver`, `isVerified`, `role`, `isSleeping`)
|
||||||
|
- primary region data (`regionId`, `regionName`) and all memberships (`regions`, `groups`)
|
||||||
|
- profile/contact fields (`firstname`, `lastname`, `email`, `address`, `postcode`, `city`, `landline`, `mobile`, `birthday`, `photo`)
|
||||||
|
- activity/pass data (`stats.weight`, `stats.count`, `lastPassDate`, `lastPassUntilValid`, `lastPassUntilValidInDays`)
|
||||||
|
- permission flags (`permissions.*`)
|
||||||
|
- coordinates (`coordinates.lat`, `coordinates.lon`)
|
||||||
|
|
||||||
|
The tool is strongly typed in `Tools/CurrentUserTools.cs`, where each field is documented with `Description` attributes so MCP clients can surface schema-aware help.
|
||||||
|
|
||||||
|
## Publishing to NuGet.org
|
||||||
|
|
||||||
|
1. Run `dotnet pack -c Release` to create the NuGet package
|
||||||
|
2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key <your-api-key> --source https://api.nuget.org/v3/index.json`
|
||||||
|
|
||||||
|
## Using the MCP Server from NuGet.org
|
||||||
|
|
||||||
|
Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org.
|
||||||
|
|
||||||
|
- **VS Code**: Create a `<WORKSPACE DIRECTORY>/.vscode/mcp.json` file
|
||||||
|
- **Visual Studio**: Create a `<SOLUTION DIRECTORY>\.mcp.json` file
|
||||||
|
|
||||||
|
For both VS Code and Visual Studio, the configuration file uses the following server definition:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"FsMcp": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "dnx",
|
||||||
|
"args": [
|
||||||
|
"<your package ID here>",
|
||||||
|
"--version",
|
||||||
|
"<your package version here>",
|
||||||
|
"--yes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## More information
|
||||||
|
|
||||||
|
.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP:
|
||||||
|
|
||||||
|
- [Official Documentation](https://modelcontextprotocol.io/)
|
||||||
|
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
|
||||||
|
- [GitHub Organization](https://github.com/modelcontextprotocol)
|
||||||
|
- [MCP C# SDK](https://modelcontextprotocol.github.io/csharp-sdk)
|
||||||
263
FsMcp/Tools/CurrentUserTools.cs
Normal file
263
FsMcp/Tools/CurrentUserTools.cs
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using FsMcp;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
internal class CurrentUserTools
|
||||||
|
{
|
||||||
|
private readonly FoodsharingApiClient _apiClient;
|
||||||
|
|
||||||
|
public CurrentUserTools(FoodsharingApiClient apiClient)
|
||||||
|
{
|
||||||
|
_apiClient = apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool]
|
||||||
|
[Description("Returns full profile details for the currently authenticated foodsharing user from /api/users/current/details, including identity, verification, membership regions/groups, permissions, contact data, coordinates, pass status, and activity stats.")]
|
||||||
|
public async Task<CurrentUserDetails> GetCurrentUserDetailsAsync()
|
||||||
|
{
|
||||||
|
await _apiClient.EnsureLoginAsync();
|
||||||
|
|
||||||
|
var userDetails = await _apiClient.HttpClient.GetFromJsonAsync<CurrentUserDetails>(Endpoints.UserCurrentDetails);
|
||||||
|
return userDetails ?? throw new InvalidOperationException("Current user details response was empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CurrentUserDetails
|
||||||
|
{
|
||||||
|
[Description("Unique user ID.")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
[Description("True if the account is a foodsaver account.")]
|
||||||
|
[JsonPropertyName("foodsaver")]
|
||||||
|
public bool Foodsaver { get; init; }
|
||||||
|
|
||||||
|
[Description("True if the account is verified.")]
|
||||||
|
[JsonPropertyName("isVerified")]
|
||||||
|
public bool IsVerified { get; init; }
|
||||||
|
|
||||||
|
[Description("Primary region ID of the user.")]
|
||||||
|
[JsonPropertyName("regionId")]
|
||||||
|
public int RegionId { get; init; }
|
||||||
|
|
||||||
|
[Description("True if sleep mode is currently enabled for the user.")]
|
||||||
|
[JsonPropertyName("isSleeping")]
|
||||||
|
public bool IsSleeping { get; init; }
|
||||||
|
|
||||||
|
[Description("Display name of the user's primary region.")]
|
||||||
|
[JsonPropertyName("regionName")]
|
||||||
|
public string RegionName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Public about-me text visible to other users.")]
|
||||||
|
[JsonPropertyName("aboutMePublic")]
|
||||||
|
public string AboutMePublic { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Mailbox ID for internal messaging.")]
|
||||||
|
[JsonPropertyName("mailboxId")]
|
||||||
|
public int MailboxId { get; init; }
|
||||||
|
|
||||||
|
[Description("True if an iCal calendar token exists for the user.")]
|
||||||
|
[JsonPropertyName("hasCalendarToken")]
|
||||||
|
public bool HasCalendarToken { get; init; }
|
||||||
|
|
||||||
|
[Description("User first name.")]
|
||||||
|
[JsonPropertyName("firstname")]
|
||||||
|
public string Firstname { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("User last name.")]
|
||||||
|
[JsonPropertyName("lastname")]
|
||||||
|
public string Lastname { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Gender value as provided by API (numeric enum).")]
|
||||||
|
[JsonPropertyName("gender")]
|
||||||
|
public int Gender { get; init; }
|
||||||
|
|
||||||
|
[Description("Path to profile photo resource.")]
|
||||||
|
[JsonPropertyName("photo")]
|
||||||
|
public string Photo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Date/time of last successful pass completion (ISO-8601 timestamp).")]
|
||||||
|
[JsonPropertyName("lastPassDate")]
|
||||||
|
public DateTimeOffset? LastPassDate { get; init; }
|
||||||
|
|
||||||
|
[Description("Date/time until pass validity remains active (ISO-8601 timestamp).")]
|
||||||
|
[JsonPropertyName("lastPassUntilValid")]
|
||||||
|
public DateTimeOffset? LastPassUntilValid { get; init; }
|
||||||
|
|
||||||
|
[Description("Remaining validity duration of the pass in whole days.")]
|
||||||
|
[JsonPropertyName("lastPassUntilValidInDays")]
|
||||||
|
public int? LastPassUntilValidInDays { get; init; }
|
||||||
|
|
||||||
|
[Description("User contribution statistics (weight and count).")]
|
||||||
|
[JsonPropertyName("stats")]
|
||||||
|
public UserStats? Stats { get; init; }
|
||||||
|
|
||||||
|
[Description("Permission flags for profile editing and administrative capabilities.")]
|
||||||
|
[JsonPropertyName("permissions")]
|
||||||
|
public UserPermissions? Permissions { get; init; }
|
||||||
|
|
||||||
|
[Description("True if an active email address is currently configured.")]
|
||||||
|
[JsonPropertyName("hasActiveEmail")]
|
||||||
|
public bool HasActiveEmail { get; init; }
|
||||||
|
|
||||||
|
[Description("Home coordinates of the user profile.")]
|
||||||
|
[JsonPropertyName("coordinates")]
|
||||||
|
public GeoCoordinates? Coordinates { get; init; }
|
||||||
|
|
||||||
|
[Description("Street address.")]
|
||||||
|
[JsonPropertyName("address")]
|
||||||
|
public string Address { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("City name.")]
|
||||||
|
[JsonPropertyName("city")]
|
||||||
|
public string City { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Postal code.")]
|
||||||
|
[JsonPropertyName("postcode")]
|
||||||
|
public string Postcode { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Email address.")]
|
||||||
|
[JsonPropertyName("email")]
|
||||||
|
public string Email { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Landline phone number.")]
|
||||||
|
[JsonPropertyName("landline")]
|
||||||
|
public string Landline { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Mobile phone number.")]
|
||||||
|
[JsonPropertyName("mobile")]
|
||||||
|
public string Mobile { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Birthday date/time as provided by API (ISO-8601 timestamp).")]
|
||||||
|
[JsonPropertyName("birthday")]
|
||||||
|
public DateTimeOffset? Birthday { get; init; }
|
||||||
|
|
||||||
|
[Description("Internal about-me text visible to authorized roles.")]
|
||||||
|
[JsonPropertyName("aboutMeIntern")]
|
||||||
|
public string AboutMeIntern { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Role identifier as provided by API (numeric enum).")]
|
||||||
|
[JsonPropertyName("role")]
|
||||||
|
public int Role { get; init; }
|
||||||
|
|
||||||
|
[Description("All regions the user is a member of, with classification and responsibility status.")]
|
||||||
|
[JsonPropertyName("regions")]
|
||||||
|
public IReadOnlyList<UserRegionMembership> Regions { get; init; } = [];
|
||||||
|
|
||||||
|
[Description("All groups the user belongs to with responsibility status.")]
|
||||||
|
[JsonPropertyName("groups")]
|
||||||
|
public IReadOnlyList<UserGroupMembership> Groups { get; init; } = [];
|
||||||
|
|
||||||
|
[Description("Optional position or role title text.")]
|
||||||
|
[JsonPropertyName("position")]
|
||||||
|
public string Position { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UserStats
|
||||||
|
{
|
||||||
|
[Description("Total saved food weight in kilograms.")]
|
||||||
|
[JsonPropertyName("weight")]
|
||||||
|
public double Weight { get; init; }
|
||||||
|
|
||||||
|
[Description("Total number of counted activities/pickups.")]
|
||||||
|
[JsonPropertyName("count")]
|
||||||
|
public int Count { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UserPermissions
|
||||||
|
{
|
||||||
|
[Description("May edit own user profile.")]
|
||||||
|
[JsonPropertyName("mayEditUserProfile")]
|
||||||
|
public bool MayEditUserProfile { get; init; }
|
||||||
|
|
||||||
|
[Description("May administrate other user profiles.")]
|
||||||
|
[JsonPropertyName("mayAdministrateUserProfile")]
|
||||||
|
public bool MayAdministrateUserProfile { get; init; }
|
||||||
|
|
||||||
|
[Description("May administrate blog posts.")]
|
||||||
|
[JsonPropertyName("administrateBlog")]
|
||||||
|
public bool AdministrateBlog { get; init; }
|
||||||
|
|
||||||
|
[Description("May edit quizzes.")]
|
||||||
|
[JsonPropertyName("editQuiz")]
|
||||||
|
public bool EditQuiz { get; init; }
|
||||||
|
|
||||||
|
[Description("May handle reports.")]
|
||||||
|
[JsonPropertyName("handleReports")]
|
||||||
|
public bool HandleReports { get; init; }
|
||||||
|
|
||||||
|
[Description("May add stores.")]
|
||||||
|
[JsonPropertyName("addStore")]
|
||||||
|
public bool AddStore { get; init; }
|
||||||
|
|
||||||
|
[Description("May edit static content.")]
|
||||||
|
[JsonPropertyName("editContent")]
|
||||||
|
public bool EditContent { get; init; }
|
||||||
|
|
||||||
|
[Description("May administrate newsletter email settings.")]
|
||||||
|
[JsonPropertyName("administrateNewsletterEmail")]
|
||||||
|
public bool AdministrateNewsletterEmail { get; init; }
|
||||||
|
|
||||||
|
[Description("May administrate regions.")]
|
||||||
|
[JsonPropertyName("administrateRegions")]
|
||||||
|
public bool AdministrateRegions { get; init; }
|
||||||
|
|
||||||
|
[Description("May perform global search.")]
|
||||||
|
[JsonPropertyName("maySearchGlobal")]
|
||||||
|
public bool MaySearchGlobal { get; init; }
|
||||||
|
|
||||||
|
[Description("May edit store categories.")]
|
||||||
|
[JsonPropertyName("editStoreCategories")]
|
||||||
|
public bool EditStoreCategories { get; init; }
|
||||||
|
|
||||||
|
[Description("May edit resource categories.")]
|
||||||
|
[JsonPropertyName("editResourceCategories")]
|
||||||
|
public bool EditResourceCategories { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record GeoCoordinates
|
||||||
|
{
|
||||||
|
[Description("Latitude in decimal degrees.")]
|
||||||
|
[JsonPropertyName("lat")]
|
||||||
|
public double Lat { get; init; }
|
||||||
|
|
||||||
|
[Description("Longitude in decimal degrees.")]
|
||||||
|
[JsonPropertyName("lon")]
|
||||||
|
public double Lon { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UserRegionMembership
|
||||||
|
{
|
||||||
|
[Description("Region ID.")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
[Description("Region display name.")]
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("Region classification numeric value from API.")]
|
||||||
|
[JsonPropertyName("classification")]
|
||||||
|
public int Classification { get; init; }
|
||||||
|
|
||||||
|
[Description("True if the user has responsible role in this region.")]
|
||||||
|
[JsonPropertyName("isResponsible")]
|
||||||
|
public bool IsResponsible { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UserGroupMembership
|
||||||
|
{
|
||||||
|
[Description("Group ID.")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
[Description("Group display name.")]
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Description("True if the user has responsible role in this group.")]
|
||||||
|
[JsonPropertyName("isResponsible")]
|
||||||
|
public bool IsResponsible { get; init; }
|
||||||
|
}
|
||||||
51
FsMcp/Tools/RandomNumberTools.cs
Normal file
51
FsMcp/Tools/RandomNumberTools.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using FsMcp;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sample MCP tools for demonstration purposes.
|
||||||
|
/// These tools can be invoked by MCP clients to perform various operations.
|
||||||
|
/// </summary>
|
||||||
|
internal class RandomNumberTools
|
||||||
|
{
|
||||||
|
private readonly FoodsharingApiClient _apiClient;
|
||||||
|
|
||||||
|
public RandomNumberTools(FoodsharingApiClient apiClient)
|
||||||
|
{
|
||||||
|
_apiClient = apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mcp Server tool to get the crsf token for the current session (for demonstration purposes).
|
||||||
|
[McpServerTool]
|
||||||
|
[Description("Gets the CSRF token for the current session.")]
|
||||||
|
public async Task<string> GetCsrfTokenAsync()
|
||||||
|
{
|
||||||
|
await _apiClient.EnsureLoginAsync();
|
||||||
|
return _apiClient.HttpClient.DefaultRequestHeaders.GetValues("X-CSRF-Token").FirstOrDefault() ?? throw new InvalidOperationException("CSRF token not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool]
|
||||||
|
[Description("Generates a random number between the specified minimum and maximum values.")]
|
||||||
|
public int GetRandomNumber(
|
||||||
|
[Description("Minimum value (inclusive)")] int min = 0,
|
||||||
|
[Description("Maximum value (exclusive)")] int max = 100)
|
||||||
|
{
|
||||||
|
return Random.Shared.Next(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool]
|
||||||
|
[Description("Generates a random Lorem Ipsum text with the specified number of words.")]
|
||||||
|
public string GetLoremIpsum(int wordCount = 10)
|
||||||
|
{
|
||||||
|
string[] loremIpsumWords = new[]
|
||||||
|
{
|
||||||
|
"Lorem", "ipsum", "dolor", "sit", "amet", "consectetur",
|
||||||
|
"adipiscing", "elit", "sed", "do", "eiusmod", "tempor",
|
||||||
|
"incididunt", "ut", "labore", "et", "dolore", "magna",
|
||||||
|
"aliqua"
|
||||||
|
};
|
||||||
|
|
||||||
|
return string.Join(" ", Enumerable.Range(0, wordCount).Select(_ => loremIpsumWords[Random.Shared.Next(loremIpsumWords.Length)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
18070
fsopenapi.json
Normal file
18070
fsopenapi.json
Normal file
File diff suppressed because it is too large
Load Diff
19
mcp.example.json
Normal file
19
mcp.example.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"FsMcp": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"FsMcp/FsMcp.csproj",
|
||||||
|
"-c",
|
||||||
|
"Debug"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"USERNAME": "your-foodsharing-username",
|
||||||
|
"PASSWORD": "your-foodsharing-password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user