We use TTMSMCPServer with TTMSMCPStreamableHttpTransport. When the server is destroyed during Delphi unit finalization, TTMSMCPWinHTTPServer.StopServer raises:
The transport and thread objects are still assigned, but shutdown occurs after relevant TMS unit-level HTTP state has begun finalizing. We currently work around this by stopping the server from an earlier unit finalization block.
Could TMS make this lifecycle safer by:
stopping or tracking live HTTP server instances before unloading global HTTP support;
making StopServer and transport destruction fully idempotent;
clearing worker-thread fields immediately after freeing them; and
avoiding an EThread exception when shutdown encounters an already-invalid thread handle?
Ideally, destroying an active TTMSMCPServer during application finalization would shut down cleanly without requiring application-specific unit-order workarounds.
program Project1;
{$APPTYPE CONSOLE}
{
Prerequisites:
- Add the Spring4D and TMS AI Studio source directories to the search path.
- Reserve http://+:8934/ or run the sample elevated.
Reproduction:
1. Run the program and wait for the server-started message.
2. Press Enter.
3. During application finalization, Spring releases the active singleton after
the TMS HTTP units have finalized and TTMSMCPWinHTTPServer.StopServer raises
EThread with "The handle is invalid (6)".
}
uses
System.SysUtils,
// Keep Spring before the TMS units: its global singleton container is then
// finalized after the TMS HTTP units, which exposes the shutdown problem.
Spring.Container,
TMS.MCP.Server,
TMS.MCP.Transport.StreamableHTTP;
type
///<summary>Minimal service contract retained by Spring's singleton lifetime manager</summary>
IMcpService = interface
['{36B31A80-B1F6-4F43-B8D3-D0C003E17D49}']
///<summary>Start the embedded Streamable HTTP MCP server</summary>
procedure Start;
end;
///<summary>Owns an active TMS MCP server until Spring destroys the singleton</summary>
TMcpService = class(TInterfacedObject, IMcpService)
private
FServer: TTMSMCPServer;
public
///<summary>Create the MCP server and its owned Streamable HTTP transport</summary>
constructor Create;
///<summary>Stop and destroy the MCP server</summary>
destructor Destroy; override;
///<summary>Start the embedded Streamable HTTP MCP server</summary>
procedure Start;
end;
constructor TMcpService.Create;
begin
inherited Create;
FServer := TTMSMCPServer.Create(nil);
FServer.ServerName := 'TMS finalization reproducer';
FServer.ServerVersion := '1.0.0';
FServer.Transport := TTMSMCPStreamableHTTPTransport.Create(FServer, 8934, '/mcp');
end;
destructor TMcpService.Destroy;
begin
try
FServer.Stop;
finally
FServer.Free;
end;
inherited;
end;
procedure TMcpService.Start;
begin
FServer.Start;
end;
var
Service: IMcpService;
begin
GlobalContainer.RegisterType<TMcpService>
.Implements<IMcpService>
.AsSingleton;
GlobalContainer.Build;
Service := GlobalContainer.Resolve<IMcpService>;
Service.Start;
WriteLn('TMS Streamable HTTP MCP server is running on port 8934.');
WriteLn('Press Enter to exit and reproduce the finalization exception.');
ReadLn;
// Spring's singleton lifetime manager deliberately remains the final owner.
Service := nil;
end.
Thank you but the Streamable HTTP shutdown issue is not entirely fixed in TMS AI Studio 1.7.4.0. TTMSMCPStreamableHTTPTransport.Start creates and starts its message, ping, and cleanup threads before setting FHttpServer.Active. If HTTP startup then fails, Stop skips all three threads because its cleanup is guarded by "if FHttpServer.Active then". Destroying the failed transport frees the objects used by those still-running threads, causing leaks and access violations. This sample forces that path by starting two servers on the same port. Please make Start exception-safe and make Stop terminate and join every assigned worker thread independently of the HTTP Active/Running state.
program Project1;
{$APPTYPE CONSOLE}
{
Prerequisites:
- Add the TMS AI Studio source directory to the search path.
- Reserve http://+:8934/ or run the sample elevated.
Reproduction:
1. The first MCP server successfully occupies port 8934.
2. The second MCP server starts its three anonymous transport threads, then
fails when its HTTP server tries to occupy the same port.
3. The failed server is freed and the program waits. Its transport destructor
skips thread cleanup because FHttpServer.Active is False, leaving the
threads running against freed locks, collections, and transport memory.
4. An access violation normally follows in one of the anonymous methods from
TTMSMCPStreamableHTTPTransport.Start.
}
uses
System.SysUtils,
System.Classes,
TMS.MCP.Server,
TMS.MCP.Transport.StreamableHTTP;
const
ReproductionPort = 8934;
type
///<summary>Runs the failed-start worker-thread leak reproduction</summary>
TFailedStartLeakReproducer = class sealed
private
///<summary>Create an MCP server configured with a Streamable HTTP transport on the reproduction port</summary>
class function DoCreateServer(const aName: string): TTMSMCPServer; static;
public
///<summary>Run the conflicting-server startup scenario and wait for leaked workers to fail</summary>
class procedure Run; static;
end;
class function TFailedStartLeakReproducer.DoCreateServer(const aName: string): TTMSMCPServer;
begin
Result := TTMSMCPServer.Create(nil);
try
Result.ServerName := aName;
Result.ServerVersion := '1.0.0';
Result.Transport := TTMSMCPStreamableHTTPTransport.Create(Result, ReproductionPort, '/mcp');
except
Result.Free;
raise;
end;
end;
class procedure TFailedStartLeakReproducer.Run;
var
aBlockerServer: TTMSMCPServer;
aFailedServer: TTMSMCPServer;
aStartFailed: Boolean;
begin
aBlockerServer := DoCreateServer('Port blocker');
try
aBlockerServer.Start;
WriteLn(Format('The first MCP server is running on port %d.', [ReproductionPort]));
aFailedServer := DoCreateServer('Expected failed start');
try
aStartFailed := False;
try
WriteLn('Starting a second MCP server on the occupied port...');
aFailedServer.Start;
except
on E: Exception do
begin
aStartFailed := True;
WriteLn('Expected startup failure: ', E.ClassName, ': ', E.Message);
end;
end;
finally
WriteLn('Freeing the second server...');
aFailedServer.Free;
end;
if (not aStartFailed) then
raise Exception.Create('The second server unexpectedly started; the port conflict did not reproduce');
WriteLn('Waiting for the leaked anonymous transport threads...');
Sleep(3000);
WriteLn('No access violation was observed during the wait, but the failed-start threads were not joined by Stop.');
finally
aBlockerServer.Free;
Readln;
end;
end;
begin
try
TFailedStartLeakReproducer.Run;
except
on E: Exception do
begin
WriteLn(E.ClassName, ': ', E.Message);
ExitCode := 1;
end;
end;
end.