Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions SlimFaas.sln
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GmailMailerApi", "src\Gmail
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalculatorApi", "src\CalculatorApi\CalculatorApi.csproj", "{C677E191-12D7-4F45-8777-32438A92CC35}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "src\ConsoleApp1\ConsoleApp1.csproj", "{FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimFaasKafka", "src\SlimFaasKafka\SlimFaasKafka.csproj", "{D771C816-4642-443B-A155-7DCA8C130864}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimFaasKafka.Tests", "tests\SlimFaasKafka.Tests\SlimFaasKafka.Tests.csproj", "{CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimFaasMcpGateway", "src\SlimFaasMcpGateway\SlimFaasMcpGateway.csproj", "{6FFC9F83-0529-487E-901D-92FB73E3EC21}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -78,10 +78,6 @@ Global
{C677E191-12D7-4F45-8777-32438A92CC35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C677E191-12D7-4F45-8777-32438A92CC35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C677E191-12D7-4F45-8777-32438A92CC35}.Release|Any CPU.Build.0 = Release|Any CPU
{FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Release|Any CPU.Build.0 = Release|Any CPU
{D771C816-4642-443B-A155-7DCA8C130864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D771C816-4642-443B-A155-7DCA8C130864}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D771C816-4642-443B-A155-7DCA8C130864}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -90,5 +86,9 @@ Global
{CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}.Release|Any CPU.Build.0 = Release|Any CPU
{6FFC9F83-0529-487E-901D-92FB73E3EC21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FFC9F83-0529-487E-901D-92FB73E3EC21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FFC9F83-0529-487E-901D-92FB73E3EC21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FFC9F83-0529-487E-901D-92FB73E3EC21}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.102",
"version": "10.0.100",
"rollForward": "latestFeature",
"allowPrerelease": true
}
Expand Down
2 changes: 1 addition & 1 deletion src/CalculatorApi/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5106",
"applicationUrl": "http://localhost:5107",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
10 changes: 0 additions & 10 deletions src/ConsoleApp1/ConsoleApp1.csproj

This file was deleted.

56 changes: 0 additions & 56 deletions src/ConsoleApp1/Program.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/SlimFaas/Endpoints/EventEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private static async Task<IResult> PublishEvent(
string baseFunctionPodUrl = Environment.GetEnvironmentVariable(EnvironmentVariables.BaseFunctionPodUrl) ??
EnvironmentVariables.BaseFunctionPodUrlDefault;

var baseUrl = SlimDataEndpoint.Get(pod, baseFunctionPodUrl);
var baseUrl = SlimDataEndpoint.Get(pod, baseFunctionPodUrl) + "/";
logger.LogDebug("Sending event {EventName} to {FunctionDeployment} at {BaseUrl} with path {FunctionPath} and query {UriComponent}",
eventName, function.Deployment, baseUrl, functionPath, context.Request.QueryString.ToUriComponent());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { McpTool, UiTool } from "./types";
import { fromBase64Utf8, isProbablyBase64, safeJsonParse, toBase64Utf8 } from "../../utils/encoding";
import "./SwaggerToMcpPage.scss";

const API_BASE = (import.meta.env.VITE_MCP_API_BASE_URL ?? "").replace(/\/+$/, "");
const API_BASE = (import.meta.env.VITE_MCP_API_BASE_URL ?? "").replace(/[/.]+$/, "");
const api = (path: string) => (API_BASE ? `${API_BASE}${path}` : path);

function clampUrlBase(baseUrl: string): string {
Expand Down Expand Up @@ -140,7 +140,10 @@ export default function SwaggerToMcpPage() {
let url = `${originBase}/mcp?openapi_url=${encodeURIComponent(swaggerUrl.trim())}`;
const b = clampUrlBase(baseUrl.trim());
if (b) url += `&base_url=${encodeURIComponent(b)}`;
if (promptB64 && isProbablyBase64(promptB64)) url += `&mcp_prompt=${encodeURIComponent(promptB64)}`;
// Ne pas ajouter mcp_prompt si c'est un objet JSON vide (e30= en base64)
if (promptB64 && isProbablyBase64(promptB64) && promptB64 !== 'e30=') {
url += `&mcp_prompt=${encodeURIComponent(promptB64)}`;
}
if (includePrm && oauthB64 && isProbablyBase64(oauthB64)) url += `&oauth=${encodeURIComponent(oauthB64)}`;
if (structuredContent) url += `&structured_content=true`;
if (toolPrefix.trim()) url += `&tool_prefix=${encodeURIComponent(toolPrefix.trim())}`;
Expand Down Expand Up @@ -308,7 +311,7 @@ export default function SwaggerToMcpPage() {

let endpoint = `/tools/${encodeURIComponent(toolName)}?openapi_url=${encodeURIComponent(swagger)}`;
if (b) endpoint += `&base_url=${encodeURIComponent(b)}`;
if (promptB64) endpoint += `&mcp_prompt=${encodeURIComponent(promptB64)}`;
if (promptB64 && promptB64 !== 'e30=') endpoint += `&mcp_prompt=${encodeURIComponent(promptB64)}`;
if (includePrm && oauthB64) endpoint += `&oauth=${encodeURIComponent(oauthB64)}`;
if (structuredContent) endpoint += `&structured_content=true`;
if (toolPrefix.trim()) endpoint += `&tool_prefix=${encodeURIComponent(toolPrefix.trim())}`;
Expand Down
183 changes: 183 additions & 0 deletions src/SlimFaasMcpGateway/Audit/JsonPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@

using System.Text.Json;
using System.Text.Json.Nodes;

namespace SlimFaasMcpGateway.Audit;

/// <summary>
/// A minimal JSON patch mechanism (not RFC6902) designed for:
/// - storing "diffs" between JSON snapshots,
/// - reconstructing snapshots by applying patches in order.
/// Arrays are treated as atomic values (whole array replace).
/// </summary>
public static class JsonPatch
{
public sealed record Op(string Path, JsonNode? Value, bool Remove);

public static IReadOnlyList<Op> Create(JsonNode? before, JsonNode? after)
{
var ops = new List<Op>();
DiffInternal(ops, "", before, after);
return ops;
}

public static JsonNode Apply(JsonNode baseNode, IEnumerable<Op> ops)
{
foreach (var op in ops)
{
ApplyOne(baseNode, op);
}
return baseNode;
}

public static string Serialize(IReadOnlyList<Op> ops)
=> JsonSerializer.Serialize(ops, AppJsonOptions.Default);

public static IReadOnlyList<Op> Deserialize(string json)
=> JsonSerializer.Deserialize<List<Op>>(json, AppJsonOptions.Default) ?? new();

/// <summary>
/// Creates a text-based unified diff between two JSON nodes using DiffPlex
/// </summary>
public static TextDiff.UnifiedDiff CreateTextDiff(JsonNode? before, JsonNode? after)
{
var beforeText = before?.ToJsonString(AppJsonOptions.DefaultIndented) ?? "";
var afterText = after?.ToJsonString(AppJsonOptions.DefaultIndented) ?? "";
return TextDiff.Create(beforeText, afterText);
}

private static void DiffInternal(List<Op> ops, string path, JsonNode? before, JsonNode? after)
{
if (JsonEquals(before, after))
return;

// null cases
if (before is null)
{
ops.Add(new Op(path, after, Remove: false));
return;
}

if (after is null)
{
ops.Add(new Op(path, Value: null, Remove: true));
return;
}

// arrays -> whole replace
if (before is JsonArray || after is JsonArray)
{
ops.Add(new Op(path, after, Remove: false));
return;
}

// objects -> key-level diff
if (before is JsonObject bObj && after is JsonObject aObj)
{
// removed keys
foreach (var kv in bObj)
{
if (!aObj.ContainsKey(kv.Key))
ops.Add(new Op(Join(path, kv.Key), Value: null, Remove: true));
}

// added/changed keys
foreach (var kv in aObj)
{
bObj.TryGetPropertyValue(kv.Key, out var bVal);
DiffInternal(ops, Join(path, kv.Key), bVal, kv.Value);
}

return;
}

// primitive replace
ops.Add(new Op(path, after, Remove: false));
}

private static void ApplyOne(JsonNode root, Op op)
{
if (string.IsNullOrEmpty(op.Path))
{
// root replace is handled by caller (we keep it simple: replace properties instead)
if (root is JsonObject rObj && op.Value is JsonObject vObj && !op.Remove)
{
rObj.Clear();
foreach (var kv in vObj) rObj[kv.Key] = kv.Value;
return;
}

throw new InvalidOperationException("Root replace not supported for non-object roots.");
}

var (parent, lastSeg) = NavigateToParent(root, op.Path);

if (parent is JsonObject pobj)
{
if (op.Remove) pobj.Remove(lastSeg);
else pobj[lastSeg] = op.Value?.DeepClone();
return;
}

throw new InvalidOperationException($"Unsupported parent node for path '{op.Path}'.");
}

private static (JsonNode Parent, string LastSegment) NavigateToParent(JsonNode root, string path)
{
// path format: /a/b/c
var segs = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segs.Length == 0) throw new InvalidOperationException("Invalid path");

JsonNode current = root;
for (var i = 0; i < segs.Length - 1; i++)
{
var seg = segs[i];
if (current is JsonObject obj)
{
if (!obj.TryGetPropertyValue(seg, out var next) || next is null)
{
next = new JsonObject();
obj[seg] = next;
}
current = next;
continue;
}

throw new InvalidOperationException($"Cannot navigate into non-object node at '{seg}'.");
}

return (current, segs[^1]);
}

private static bool JsonEquals(JsonNode? a, JsonNode? b)
{
if (ReferenceEquals(a, b)) return true;
if (a is null || b is null) return false;

var aj = a.ToJsonString(AppJsonOptions.Default);
var bj = b.ToJsonString(AppJsonOptions.Default);
return string.Equals(aj, bj, StringComparison.Ordinal);
}

private static string Join(string path, string seg) => string.IsNullOrEmpty(path) ? "/" + seg : path + "/" + seg;
}

/// <summary>
/// Shared JsonSerializerOptions (AOT friendly).
/// </summary>
public static class AppJsonOptions
{
public static readonly JsonSerializerOptions Default = new(System.Text.Json.JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
WriteIndented = false,
TypeInfoResolverChain = { SlimFaasMcpGateway.Serialization.ApiJsonContext.Default }
};

public static readonly JsonSerializerOptions DefaultIndented = new(System.Text.Json.JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
WriteIndented = true,
TypeInfoResolverChain = { SlimFaasMcpGateway.Serialization.ApiJsonContext.Default }
};
}
Loading
Loading