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
21 changes: 16 additions & 5 deletions dotnet/src/dotnetcore/GxNetCoreStartup/GxNetCoreStartup.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
<EnableDefaultContentItems>false</EnableDefaultContentItems>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CustomContentTarget</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);NU5104</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<WarningsNotAsErrors>NU1900;NU1901;NU1902;NU1903;NU1904;NU5104</WarningsNotAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<WarningsNotAsErrors>NU1900;NU1901;NU1902;NU1903;NU1904;NU5104</WarningsNotAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="8.0.3" />
Expand All @@ -27,12 +36,14 @@
<PackageReference Include="itext7" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="itext7.font-asian" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="itext7.pdfhtml" Version="5.0.0" PrivateAssets="All" />

<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.4" PrivateAssets="All"/>
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.4" PrivateAssets="All"/>
<PackageReference Include="ModelContextProtocol.Core" Version="0.3.0-preview.4" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GxClasses.Web\GxClasses.Web.csproj" />
<ProjectReference Include="..\GxClasses\GxClasses.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GxClasses.Web\GxClasses.Web.csproj" />
<ProjectReference Include="..\GxClasses\GxClasses.csproj" />
</ItemGroup>

<Target Name="CustomContentTarget">
<ItemGroup>
Expand Down
94 changes: 38 additions & 56 deletions dotnet/src/dotnetcore/GxNetCoreStartup/RunUtils.cs
Original file line number Diff line number Diff line change
@@ -1,72 +1,54 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace GeneXus.Application
{

public static class GxRunner

internal static class FileTools
{
public static void RunAsync(
string commandLine,
string workingDir,
string virtualPath,
string schema,
Action<int> onExit = null)
static readonly IGXLogger log = GXLoggerFactory.GetLogger(typeof(FileTools).FullName);
public static List<Assembly> MCPFileTools(string baseDirectory)
{
var stdout = new StringBuilder();
var stderr = new StringBuilder();

using var proc = new Process
// List of Assemblies with MCP tools
List<Assembly> mcpAssemblies = new();
string currentBin = Path.Combine(baseDirectory, "bin");
if (!Directory.Exists(currentBin))
{
StartInfo = new ProcessStartInfo
currentBin = baseDirectory;
if (!Directory.Exists(currentBin))
{
FileName = commandLine,
WorkingDirectory = workingDir,
UseShellExecute = false, // required for redirection
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = false, // flip to true only if you need to write to stdin
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
},
EnableRaisingEvents = true
};

proc.StartInfo.ArgumentList.Add(virtualPath);
proc.StartInfo.ArgumentList.Add(schema);

proc.OutputDataReceived += (_, e) =>
{
if (e.Data is null) return;
stdout.AppendLine(e.Data);
Console.WriteLine(e.Data); // forward to parent console (stdout)
};

proc.ErrorDataReceived += (_, e) =>
{
if (e.Data is null) return;
stderr.AppendLine(e.Data);
Console.Error.WriteLine(e.Data); // forward to parent console (stderr)
};

proc.Exited += (sender, e) =>
currentBin = "";
}
}
if (!String.IsNullOrEmpty(currentBin))
{
var p = (Process)sender!;
int exitCode = p.ExitCode;
p.Dispose();

Console.WriteLine($"[{DateTime.Now:T}] Process exited with code {exitCode}");

// Optional: call user-provided callback
onExit?.Invoke(exitCode);
};

if (!proc.Start())
throw new InvalidOperationException("Failed to start process");
Console.WriteLine($"[{DateTime.Now:T}] MCP Server Started.");
GXLogging.Info(log, $"[{DateTime.Now:T}] Registering MCP tools.");
List<string> L = Directory.GetFiles(currentBin, "*mcp_service.dll").ToList<string>();
foreach (string mcpFile in L)
{
var assembly = Assembly.LoadFrom(mcpFile);
foreach (var tool in assembly.GetTypes())
{
// Each MCP Assembly is added to the list to return for registration
var attributes = tool.GetCustomAttributes().Where(att => att.ToString() == "ModelContextProtocol.Server.McpServerToolTypeAttribute");
if (attributes != null && attributes.Any())
{
GXLogging.Info(log, $"[{DateTime.Now:T}] Loading tool {mcpFile}.");
mcpAssemblies.Add(assembly);
}
}
}
}
return mcpAssemblies;
}
}



}
67 changes: 67 additions & 0 deletions dotnet/src/dotnetcore/GxNetCoreStartup/StartMcp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Linq;
using System.Reflection;
using System.Security.Policy;
using ModelContextProtocol.AspNetCore;
using GeneXus.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Builder;

namespace GeneXus.Application
{
public class StartupMcp
{
static readonly IGXLogger log = GXLoggerFactory.GetLogger(typeof(StartupMcp).FullName);
public static void AddService(IServiceCollection services)
{
Console.Out.WriteLine("Starting MCP Server...");
var mcp = services.AddMcpServer(options =>
{
options.ServerInfo = new ModelContextProtocol.Protocol.Implementation
{
Name = "GxMcpServer",
Version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "1.0.0"
};
})
.WithHttpTransport(transportOptions =>
{
// SSE endpoints (/sse, /message) require STATEFUL sessions to support server-to-client push
transportOptions.Stateless = false;
transportOptions.IdleTimeout = TimeSpan.FromMinutes(5);
GXLogging.Debug(log, "MCP HTTP Transport configured: Stateless=false (SSE enabled), IdleTimeout=5min");
});

try
{
var mcpAssemblies = FileTools.MCPFileTools(Startup.LocalPath).ToList();
foreach (var assembly in mcpAssemblies)
{
try
{
mcp.WithToolsFromAssembly(assembly);
GXLogging.Debug(log, $"Successfully loaded MCP tools from assembly: {assembly.FullName}");
}
catch (Exception assemblyEx)
{
GXLogging.Error(log, $"Failed to load MCP tools from assembly: {assembly.FullName}", assemblyEx);
}
}
}
catch (Exception ex)
{
GXLogging.Error(log, "Error discovering MCP tool assemblies", ex);
}
}

public static void MapEndpoints(IEndpointRouteBuilder endpoints)
{
// Register MCP endpoints at root, exposing /sse and /message
endpoints.MapMcp();
GXLogging.Debug(log, "MCP Routing configured.");

}
}
}
Loading
Loading