diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/GxNetCoreStartup.csproj b/dotnet/src/dotnetcore/GxNetCoreStartup/GxNetCoreStartup.csproj
index 72b8aa3c7..d9c3fbf25 100644
--- a/dotnet/src/dotnetcore/GxNetCoreStartup/GxNetCoreStartup.csproj
+++ b/dotnet/src/dotnetcore/GxNetCoreStartup/GxNetCoreStartup.csproj
@@ -8,6 +8,15 @@
false
$(TargetsForTfmSpecificContentInPackage);CustomContentTarget
+
+ $(NoWarn);NU5104
+
+
+ NU1900;NU1901;NU1902;NU1903;NU1904;NU5104
+
+
+ NU1900;NU1901;NU1902;NU1903;NU1904;NU5104
+
@@ -27,12 +36,14 @@
-
+
+
+
+
+
+
+
-
-
-
-
diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/RunUtils.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/RunUtils.cs
index 4907d0209..08a1ed1f7 100644
--- a/dotnet/src/dotnetcore/GxNetCoreStartup/RunUtils.cs
+++ b/dotnet/src/dotnetcore/GxNetCoreStartup/RunUtils.cs
@@ -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 onExit = null)
+ static readonly IGXLogger log = GXLoggerFactory.GetLogger(typeof(FileTools).FullName);
+ public static List MCPFileTools(string baseDirectory)
{
- var stdout = new StringBuilder();
- var stderr = new StringBuilder();
-
- using var proc = new Process
+ // List of Assemblies with MCP tools
+ List 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 L = Directory.GetFiles(currentBin, "*mcp_service.dll").ToList();
+ 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;
}
}
+
+
}
diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/StartMcp.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/StartMcp.cs
new file mode 100644
index 000000000..f466076e0
--- /dev/null
+++ b/dotnet/src/dotnetcore/GxNetCoreStartup/StartMcp.cs
@@ -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()?.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.");
+
+ }
+ }
+}
diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
index 7e11e9e7b..f6510b89d 100644
--- a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
+++ b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
@@ -12,6 +12,7 @@
using GeneXus.Services.OpenTelemetry;
using GeneXus.Utils;
using GxClasses.Web.Middleware;
+
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
@@ -36,6 +37,7 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
+
using StackExchange.Redis;
namespace GeneXus.Application
@@ -59,6 +61,8 @@ public static void Main(string[] args)
port = args[2];
if (args.Length > 3 && Uri.UriSchemeHttps.Equals(args[3], StringComparison.OrdinalIgnoreCase))
schema = Uri.UriSchemeHttps;
+ if (args.Length > 4)
+ Startup.IsMcp = args[4].Equals("mcp", StringComparison.OrdinalIgnoreCase);
}
else
{
@@ -66,8 +70,6 @@ public static void Main(string[] args)
}
- MCPTools.ServerStart(Startup.VirtualPath, Startup.LocalPath, schema);
-
if (port == DEFAULT_PORT)
{
BuildWebHost(null).Run();
@@ -167,6 +169,7 @@ public class Startup
const long DEFAULT_MAX_FILE_UPLOAD_SIZE_BYTES = 528000000;
public static string VirtualPath = string.Empty;
public static string LocalPath = Directory.GetCurrentDirectory();
+ public static bool IsMcp = false;
internal static string APP_SETTINGS = "appsettings.json";
const string UrlTemplateControllerWithParms = "controllerWithParms";
@@ -277,26 +280,31 @@ public void ConfigureServices(IServiceCollection services)
options.SuppressXFrameOptionsHeader = true;
});
}
+ if (Startup.IsMcp)
+ {
+ StartupMcp.AddService(services);
+ }
+
services.AddDirectoryBrowser();
if (GXUtil.CompressResponse())
{
services.AddResponseCompression(options =>
{
options.MimeTypes = new[]
- {
- // Default
- "text/plain",
- "text/css",
- "application/javascript",
- "text/html",
- "application/xml",
- "text/xml",
- "application/json",
- "text/json",
- // Custom
- "application/json",
- "application/pdf"
- };
+ {
+ // Default
+ "text/plain",
+ "text/css",
+ "application/javascript",
+ "text/html",
+ "application/xml",
+ "text/xml",
+ "application/json",
+ "text/json",
+ // Custom
+ "application/json",
+ "application/pdf"
+ };
options.EnableForHttps = true;
});
}
@@ -374,9 +382,9 @@ private void RegisterRestServices(IMvcBuilder mvcBuilder)
try
{
string[] controllerAssemblyQualifiedName = new string(File.ReadLines(svcFile).First().SkipWhile(c => c != '"')
- .Skip(1)
- .TakeWhile(c => c != '"')
- .ToArray()).Trim().Split(',');
+ .Skip(1)
+ .TakeWhile(c => c != '"')
+ .ToArray()).Trim().Split(',');
string controllerAssemblyName = controllerAssemblyQualifiedName.Last();
if (!serviceAssemblies.Contains(controllerAssemblyName))
{
@@ -437,17 +445,17 @@ private void DefineCorsPolicy(IServiceCollection services)
services.AddCors(options =>
{
options.AddPolicy(name: CORS_POLICY_NAME,
- policy =>
- {
- policy.WithOrigins(origins);
- if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN))
- {
- policy.AllowCredentials();
- }
- policy.AllowAnyHeader();
- policy.AllowAnyMethod();
- policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS));
- });
+ policy =>
+ {
+ policy.WithOrigins(origins);
+ if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN))
+ {
+ policy.AllowCredentials();
+ }
+ policy.AllowAnyHeader();
+ policy.AllowAnyMethod();
+ policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS));
+ });
});
}
}
@@ -501,7 +509,6 @@ private void ConfigureSessionService(IServiceCollection services, ISessionServic
}
else
{
-
services.AddDistributedSqlServerCache(options =>
{
GXLogging.Info(log, $"Using SQLServer for Distributed session, ConnectionString:{sessionService.ConnectionString}, SchemaName: {sessionService.Schema}, TableName: {sessionService.TableName}");
@@ -586,11 +593,16 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
Predicate = check => check.Tags.Contains("live")
});
- endpoints.MapHealthChecks($"{baseVirtualPath }/_gx/health/ready", new HealthCheckOptions
+ endpoints.MapHealthChecks($"{baseVirtualPath}/_gx/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
+ if (Startup.IsMcp)
+ {
+ StartupMcp.MapEndpoints(endpoints);
+ }
});
+
if (log.IsCriticalEnabled && env.IsDevelopment())
{
try
@@ -704,8 +716,9 @@ private void ConfigureSwaggerUI(IApplicationBuilder app, string baseVirtualPath)
{
try
{
- string baseVirtualPathWithSep = string.IsNullOrEmpty(baseVirtualPath) ? string.Empty: $"{baseVirtualPath.TrimStart('/')}/";
- foreach (string yaml in Directory.GetFiles(LocalPath, "*.yaml")) {
+ string baseVirtualPathWithSep = string.IsNullOrEmpty(baseVirtualPath) ? string.Empty : $"{baseVirtualPath.TrimStart('/')}/";
+ foreach (string yaml in Directory.GetFiles(LocalPath, "*.yaml"))
+ {
FileInfo finfo = new FileInfo(yaml);
app.UseSwaggerUI(options =>
@@ -867,36 +880,6 @@ public void Apply(ApplicationModel application)
}
}
- static class MCPTools
- {
- public static void ServerStart(string virtualPath, string workingDir, string schema)
- {
- IGXLogger log = GXLoggerFactory.GetLogger(typeof(CustomBadRequestObjectResult).FullName);
- bool isMpcServer = false;
- try
- {
- List L = Directory.GetFiles(Path.Combine(workingDir,"bin"), "*mcp_service.dll").ToList();
- isMpcServer = L.Count > 0;
- if (isMpcServer)
- {
- GXLogging.Info(log, "Start MCP Server");
-
- GxRunner.RunAsync("GxMcpStartup.exe", Path.Combine(workingDir,"bin"), virtualPath, schema, onExit: exitCode =>
- {
- if (exitCode == 0)
- Console.WriteLine("Process completed successfully.");
- else
- Console.Error.WriteLine($"Process failed (exit code {exitCode})");
- });
- }
- }
- catch (Exception ex)
- {
- GXLogging.Error(log, "Error starting MCP Server", ex);
- }
- }
- }
-
internal class HomeControllerConvention : IApplicationModelConvention
{