Skip to content

Commit ffe70a8

Browse files
AlejandroPa-panizza_globantclaudiamurialdo
authored
- Join MCP Server and Web server in the same application. (#1211)
* - Join MCP Server and Web server in the same application. - Add conditional command line parameter to enable4/disable Mcp use * Set IdleTimeout to 30 seconds for transport options * - Extract Mcp code to a different source for incremental loading and code separation. * Remove unused usings. --------- Co-authored-by: a-panizza_globant <a.panizza@globant.com> Co-authored-by: claudiamurialdo <c.murialdo@globant.com>
1 parent a46362b commit ffe70a8

File tree

4 files changed

+139
-97
lines changed

4 files changed

+139
-97
lines changed

dotnet/src/dotnetcore/GxNetCoreStartup/GxNetCoreStartup.csproj

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
<EnableDefaultContentItems>false</EnableDefaultContentItems>
99
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CustomContentTarget</TargetsForTfmSpecificContentInPackage>
1010
</PropertyGroup>
11+
<PropertyGroup>
12+
<NoWarn>$(NoWarn);NU5104</NoWarn>
13+
</PropertyGroup>
14+
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
15+
<WarningsNotAsErrors>NU1900;NU1901;NU1902;NU1903;NU1904;NU5104</WarningsNotAsErrors>
16+
</PropertyGroup>
17+
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
18+
<WarningsNotAsErrors>NU1900;NU1901;NU1902;NU1903;NU1904;NU5104</WarningsNotAsErrors>
19+
</PropertyGroup>
1120

1221
<ItemGroup>
1322
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="8.0.3" />
@@ -27,12 +36,14 @@
2736
<PackageReference Include="itext7" Version="8.0.0" PrivateAssets="All" />
2837
<PackageReference Include="itext7.font-asian" Version="8.0.0" PrivateAssets="All" />
2938
<PackageReference Include="itext7.pdfhtml" Version="5.0.0" PrivateAssets="All" />
30-
39+
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.4" PrivateAssets="All"/>
40+
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.4" PrivateAssets="All"/>
41+
<PackageReference Include="ModelContextProtocol.Core" Version="0.3.0-preview.4" PrivateAssets="All"/>
42+
</ItemGroup>
43+
<ItemGroup>
44+
<ProjectReference Include="..\GxClasses.Web\GxClasses.Web.csproj" />
45+
<ProjectReference Include="..\GxClasses\GxClasses.csproj" />
3146
</ItemGroup>
32-
<ItemGroup>
33-
<ProjectReference Include="..\GxClasses.Web\GxClasses.Web.csproj" />
34-
<ProjectReference Include="..\GxClasses\GxClasses.csproj" />
35-
</ItemGroup>
3647

3748
<Target Name="CustomContentTarget">
3849
<ItemGroup>
Lines changed: 38 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,54 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Reflection;
37
using System.Text;
48

59
namespace GeneXus.Application
610
{
711

8-
public static class GxRunner
12+
13+
internal static class FileTools
914
{
10-
public static void RunAsync(
11-
string commandLine,
12-
string workingDir,
13-
string virtualPath,
14-
string schema,
15-
Action<int> onExit = null)
15+
static readonly IGXLogger log = GXLoggerFactory.GetLogger(typeof(FileTools).FullName);
16+
public static List<Assembly> MCPFileTools(string baseDirectory)
1617
{
17-
var stdout = new StringBuilder();
18-
var stderr = new StringBuilder();
19-
20-
using var proc = new Process
18+
// List of Assemblies with MCP tools
19+
List<Assembly> mcpAssemblies = new();
20+
string currentBin = Path.Combine(baseDirectory, "bin");
21+
if (!Directory.Exists(currentBin))
2122
{
22-
StartInfo = new ProcessStartInfo
23+
currentBin = baseDirectory;
24+
if (!Directory.Exists(currentBin))
2325
{
24-
FileName = commandLine,
25-
WorkingDirectory = workingDir,
26-
UseShellExecute = false, // required for redirection
27-
CreateNoWindow = true,
28-
RedirectStandardOutput = true,
29-
RedirectStandardError = true,
30-
RedirectStandardInput = false, // flip to true only if you need to write to stdin
31-
StandardOutputEncoding = Encoding.UTF8,
32-
StandardErrorEncoding = Encoding.UTF8
33-
},
34-
EnableRaisingEvents = true
35-
};
36-
37-
proc.StartInfo.ArgumentList.Add(virtualPath);
38-
proc.StartInfo.ArgumentList.Add(schema);
39-
40-
proc.OutputDataReceived += (_, e) =>
41-
{
42-
if (e.Data is null) return;
43-
stdout.AppendLine(e.Data);
44-
Console.WriteLine(e.Data); // forward to parent console (stdout)
45-
};
46-
47-
proc.ErrorDataReceived += (_, e) =>
48-
{
49-
if (e.Data is null) return;
50-
stderr.AppendLine(e.Data);
51-
Console.Error.WriteLine(e.Data); // forward to parent console (stderr)
52-
};
53-
54-
proc.Exited += (sender, e) =>
26+
currentBin = "";
27+
}
28+
}
29+
if (!String.IsNullOrEmpty(currentBin))
5530
{
56-
var p = (Process)sender!;
57-
int exitCode = p.ExitCode;
58-
p.Dispose();
59-
60-
Console.WriteLine($"[{DateTime.Now:T}] Process exited with code {exitCode}");
61-
62-
// Optional: call user-provided callback
63-
onExit?.Invoke(exitCode);
64-
};
65-
66-
if (!proc.Start())
67-
throw new InvalidOperationException("Failed to start process");
68-
Console.WriteLine($"[{DateTime.Now:T}] MCP Server Started.");
31+
GXLogging.Info(log, $"[{DateTime.Now:T}] Registering MCP tools.");
32+
List<string> L = Directory.GetFiles(currentBin, "*mcp_service.dll").ToList<string>();
33+
foreach (string mcpFile in L)
34+
{
35+
var assembly = Assembly.LoadFrom(mcpFile);
36+
foreach (var tool in assembly.GetTypes())
37+
{
38+
// Each MCP Assembly is added to the list to return for registration
39+
var attributes = tool.GetCustomAttributes().Where(att => att.ToString() == "ModelContextProtocol.Server.McpServerToolTypeAttribute");
40+
if (attributes != null && attributes.Any())
41+
{
42+
GXLogging.Info(log, $"[{DateTime.Now:T}] Loading tool {mcpFile}.");
43+
mcpAssemblies.Add(assembly);
44+
}
45+
}
46+
}
47+
}
48+
return mcpAssemblies;
6949
}
7050
}
7151

52+
53+
7254
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Linq;
3+
using System.Reflection;
4+
using System.Security.Policy;
5+
using ModelContextProtocol.AspNetCore;
6+
using GeneXus.Utils;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.AspNetCore;
9+
using Microsoft.AspNetCore.Routing;
10+
using Microsoft.AspNetCore.Builder;
11+
12+
namespace GeneXus.Application
13+
{
14+
public class StartupMcp
15+
{
16+
static readonly IGXLogger log = GXLoggerFactory.GetLogger(typeof(StartupMcp).FullName);
17+
public static void AddService(IServiceCollection services)
18+
{
19+
Console.Out.WriteLine("Starting MCP Server...");
20+
var mcp = services.AddMcpServer(options =>
21+
{
22+
options.ServerInfo = new ModelContextProtocol.Protocol.Implementation
23+
{
24+
Name = "GxMcpServer",
25+
Version = Assembly.GetExecutingAssembly()
26+
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "1.0.0"
27+
};
28+
})
29+
.WithHttpTransport(transportOptions =>
30+
{
31+
// SSE endpoints (/sse, /message) require STATEFUL sessions to support server-to-client push
32+
transportOptions.Stateless = false;
33+
transportOptions.IdleTimeout = TimeSpan.FromMinutes(5);
34+
GXLogging.Debug(log, "MCP HTTP Transport configured: Stateless=false (SSE enabled), IdleTimeout=5min");
35+
});
36+
37+
try
38+
{
39+
var mcpAssemblies = FileTools.MCPFileTools(Startup.LocalPath).ToList();
40+
foreach (var assembly in mcpAssemblies)
41+
{
42+
try
43+
{
44+
mcp.WithToolsFromAssembly(assembly);
45+
GXLogging.Debug(log, $"Successfully loaded MCP tools from assembly: {assembly.FullName}");
46+
}
47+
catch (Exception assemblyEx)
48+
{
49+
GXLogging.Error(log, $"Failed to load MCP tools from assembly: {assembly.FullName}", assemblyEx);
50+
}
51+
}
52+
}
53+
catch (Exception ex)
54+
{
55+
GXLogging.Error(log, "Error discovering MCP tool assemblies", ex);
56+
}
57+
}
58+
59+
public static void MapEndpoints(IEndpointRouteBuilder endpoints)
60+
{
61+
// Register MCP endpoints at root, exposing /sse and /message
62+
endpoints.MapMcp();
63+
GXLogging.Debug(log, "MCP Routing configured.");
64+
65+
}
66+
}
67+
}

dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using GeneXus.Services.OpenTelemetry;
1313
using GeneXus.Utils;
1414
using GxClasses.Web.Middleware;
15+
1516
using Microsoft.AspNetCore;
1617
using Microsoft.AspNetCore.Antiforgery;
1718
using Microsoft.AspNetCore.Builder;
@@ -60,15 +61,15 @@ public static void Main(string[] args)
6061
port = args[2];
6162
if (args.Length > 3 && Uri.UriSchemeHttps.Equals(args[3], StringComparison.OrdinalIgnoreCase))
6263
schema = Uri.UriSchemeHttps;
64+
if (args.Length > 4)
65+
Startup.IsMcp = args[4].Equals("mcp", StringComparison.OrdinalIgnoreCase);
6366
}
6467
else
6568
{
6669
LocatePhysicalLocalPath();
6770

6871
}
6972

70-
MCPTools.ServerStart(Startup.VirtualPath, Startup.LocalPath, schema);
71-
7273
if (port == DEFAULT_PORT)
7374
{
7475
BuildWebHost(null).Run();
@@ -168,6 +169,7 @@ public class Startup
168169
const long DEFAULT_MAX_FILE_UPLOAD_SIZE_BYTES = 528000000;
169170
public static string VirtualPath = string.Empty;
170171
public static string LocalPath = Directory.GetCurrentDirectory();
172+
public static bool IsMcp = false;
171173
internal static string APP_SETTINGS = "appsettings.json";
172174

173175
const string UrlTemplateControllerWithParms = "controllerWithParms";
@@ -281,6 +283,11 @@ public void ConfigureServices(IServiceCollection services)
281283
options.SuppressXFrameOptionsHeader = true;
282284
});
283285
}
286+
if (Startup.IsMcp)
287+
{
288+
StartupMcp.AddService(services);
289+
}
290+
284291
services.AddDirectoryBrowser();
285292
if (GXUtil.CompressResponse())
286293
{
@@ -505,7 +512,6 @@ private void ConfigureSessionService(IServiceCollection services, ISessionServic
505512
}
506513
else
507514
{
508-
509515
services.AddDistributedSqlServerCache(options =>
510516
{
511517
GXLogging.Info(log, $"Using SQLServer for Distributed session, ConnectionString:{sessionService.ConnectionString}, SchemaName: {sessionService.Schema}, TableName: {sessionService.TableName}");
@@ -601,11 +607,16 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
601607
Predicate = check => check.Tags.Contains("live")
602608
});
603609

604-
endpoints.MapHealthChecks($"{baseVirtualPath }/_gx/health/ready", new HealthCheckOptions
610+
endpoints.MapHealthChecks($"{baseVirtualPath}/_gx/health/ready", new HealthCheckOptions
605611
{
606612
Predicate = check => check.Tags.Contains("ready")
607613
});
614+
if (Startup.IsMcp)
615+
{
616+
StartupMcp.MapEndpoints(endpoints);
617+
}
608618
});
619+
609620
if (log.IsCriticalEnabled && env.IsDevelopment())
610621
{
611622
try
@@ -719,8 +730,9 @@ private void ConfigureSwaggerUI(IApplicationBuilder app, string baseVirtualPath)
719730
{
720731
try
721732
{
722-
string baseVirtualPathWithSep = string.IsNullOrEmpty(baseVirtualPath) ? string.Empty: $"{baseVirtualPath.TrimStart('/')}/";
723-
foreach (string yaml in Directory.GetFiles(LocalPath, "*.yaml")) {
733+
string baseVirtualPathWithSep = string.IsNullOrEmpty(baseVirtualPath) ? string.Empty : $"{baseVirtualPath.TrimStart('/')}/";
734+
foreach (string yaml in Directory.GetFiles(LocalPath, "*.yaml"))
735+
{
724736
FileInfo finfo = new FileInfo(yaml);
725737

726738
app.UseSwaggerUI(options =>
@@ -883,36 +895,6 @@ public void Apply(ApplicationModel application)
883895
}
884896
}
885897

886-
static class MCPTools
887-
{
888-
public static void ServerStart(string virtualPath, string workingDir, string schema)
889-
{
890-
IGXLogger log = GXLoggerFactory.GetLogger(typeof(CustomBadRequestObjectResult).FullName);
891-
bool isMpcServer = false;
892-
try
893-
{
894-
List<string> L = Directory.GetFiles(Path.Combine(workingDir,"bin"), "*mcp_service.dll").ToList<string>();
895-
isMpcServer = L.Count > 0;
896-
if (isMpcServer)
897-
{
898-
GXLogging.Info(log, "Start MCP Server");
899-
900-
GxRunner.RunAsync("GxMcpStartup.exe", Path.Combine(workingDir,"bin"), virtualPath, schema, onExit: exitCode =>
901-
{
902-
if (exitCode == 0)
903-
Console.WriteLine("Process completed successfully.");
904-
else
905-
Console.Error.WriteLine($"Process failed (exit code {exitCode})");
906-
});
907-
}
908-
}
909-
catch (Exception ex)
910-
{
911-
GXLogging.Error(log, "Error starting MCP Server", ex);
912-
}
913-
}
914-
}
915-
916898

917899
internal class HomeControllerConvention : IApplicationModelConvention
918900
{

0 commit comments

Comments
 (0)