Streamable HTTP MCP transport raises EThread during application finalization

We use TTMSMCPServer with TTMSMCPStreamableHttpTransport. When the server is destroyed during Delphi unit finalization, TTMSMCPWinHTTPServer.StopServer raises:

EThread: The handle is invalid (6)

Stack:

TTMSMCPWinHTTPServer.StopServer
TTMSMCPWinHTTPServer.SetActive
TTMSMCPCustomHTTPServer.SetActive
TTMSMCPStreamableHTTPTransport.Stop
TTMSMCPCustomServer.Stop

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.

Hi,

Would it be possible to provide a small sample that reproduces this?

See:

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, we'll check and report back.

Hi,

We applied some improvements that will be part of the next update.

@Tunde Thank you. When is the next update expected ?

We are preparing to get the latest changes out today. Stay tuned.

Hi,

TMS AI Studio 1.7.4.0 has been released. Please try with this new version and let us know if you still encounter issues.

Hi,

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.

Hi,

A fix for this was included in today's 1.7.4.1 release.