<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">	<channel>	<title>TMS Software</title>	<link>https://www.tmssoftware.com</link>	<language>en</language>	<pubDate>Fri, 14 Aug 2026 14:49:00 +0100</pubDate>	<docs>http://blogs.law.harvard.edu/tech/rss</docs>	<ttl>1440</ttl>	<generator>TMS Software</generator>	<image>		<url>https://www.tmssoftware.com/site/img/tmslogo.png</url>		<title>TMS Software</title>		<link>https://www.tmssoftware.com</link>	</image>
	<item>
		<title><![CDATA[﻿A technical look at the TMS AI City Guide summer project]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2509</link>
		<author>Bruno Fierens</author>
		<pubDate>Thu, 13 Aug 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		
<p style="margin-left: 25px;"><img src="https://www.tmssoftware.com/site/img/blog/SummerProject2026/tms-summer-2026-header.jpg" style="width: 100%;" alt="TMS Software Delphi Components tmsaistudio"></p><br><a href="https://www.tmssoftware.com/site/blog.asp?post=2504" target="_blank">The TMS AI City Guide project</a> is a <b>Delphi FireMonkey application</b> that combines interactive maps, AI-generated points of interest, GPX route export, and AI-generated speech. It demonstrates a useful pattern for cross-platform Delphi development: keep the application logic platform-independent and isolate native functionality behind small wrapper units.<div><br></div><div>The project targets Android, iOS, Windows and MacOS. The main application is defined in TMSCityGuide.dpr, while the UI logic is separated into UILogic.pas. Two particularly reusable examples are the file share functionality in uShareFile.pas and MP3 playback in uMP3Play.pas.</div><div><br></div><h4>Exporting a GPX file through the device share sheet</h4><br><div>The map component can export the current route, including its waypoints, to a GPX file using SaveToGPXFile. The resulting file can then be passed to other applications such as mapping, navigation, fitness, or file-management apps.</div><div><br></div><div style="text-align: center; "><img src="https://www.tmssoftware.com/site/img/blog/SummerProject2026/Summer2026ShareSheet.PNG" style="width: 25%;"><br></div><div><br></div><div>The form code first creates a temporary filename, exports the route, and calls one platform-independent procedure:<br></div><div><pre class="delphi" name="code">procedure TCityGuide.BtnShareClick(Sender: TObject);
var
  LFileName: string;
  FName: string;
begin
  if UILogic.City &lt;&gt; &#39;&#39; then
    FName := UILogic.City + &#39;.gpx&#39;
  else
    FName := &#39;mycitytour.gpx&#39;;

  LFileName := TPath.Combine(
    TPath.GetTempPath,
    TPath.GetFileName(FName)
  );

  UILogic.ExportToGPX(TMSFNCMaps, LFileName);

  ShareFile(LFileName, &#39;application/xml&#39;, FName);
end;<br>
</pre><br></div><div>The important design decision is that the form does not contain Android or iOS code. It only knows that ShareFile accepts a filename, MIME type, and title.</div><div><br></div><div><b>Android implementation</b></div><div><br></div><div>On Android, ShareFile creates an ACTION_SEND intent. The file is exposed through an AndroidX FileProvider, which produces a secure content URI. The URI is attached to the intent as EXTRA_STREAM, and read permission is granted to the receiving application.</div><div><pre class="delphi" name="code">LIntent := TJIntent.Create;
LIntent.setAction(TJIntent.JavaClass.ACTION_SEND);
LIntent.setType(StringToJString(AMimeType));

LIntent.putExtra(
  TJIntent.JavaClass.EXTRA_STREAM,
  TJParcelable.Wrap((LUri as ILocalObject).GetObjectID)
);

LIntent.addFlags(
  TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION
);

TAndroidHelper.Activity.startActivity(
  TJIntent.JavaClass.createChooser(
    LIntent,
    StrToJCharSequence(ATitle)
  )
);</pre><br></div><div>The Android project configuration also needs a FileProvider entry in AndroidManifest.template.xml and a deployed provider_paths.xml. The provider path maps the application&#146;s documents directory so that the file can safely be shared without exposing arbitrary filesystem paths.</div><div><br></div><div><b>iOS implementation</b></div><div><br></div><div>On iOS, the same procedure uses UIActivityViewController, the standard share sheet. The local filename is converted to an NSURL and passed as an activity item:</div><div><pre class="delphi" name="code">LURL := TNSURL.Wrap(
  TNSURL.OCClass.fileURLWithPath(
    StrToNSStr(AFilePath)
  )
);

LItems := TNSMutableArray.Create;
LItems.addObject((LURL as ILocalObject).GetObjectID);

LActivity := TUIActivityViewController.Wrap(
  TUIActivityViewController.Alloc.initWithActivityItems(
    LItems,
    nil
  )
);

LRootVC.presentViewController(LActivity, True, nil);</pre><br></div><div>The implementation also configures a popover source view and rectangle when required. This is important for iPad compatibility, where an activity controller must have a presentation source.</div><div><br></div><div>The result is a single reusable API:</div><div><pre class="delphi" name="code">ShareFile(
  &#39;/path/to/citytour.gpx&#39;,
  &#39;application/xml&#39;,
  &#39;Share city tour&#39;
);
</pre></div><div>On desktop platforms, the same wrapper falls back to a TSaveDialog. This is a useful cross-platform technique: expose one common interface while using conditional compilation only inside the implementation unit.</div><div><br></div><h4>Converting AI-generated descriptions into speech</h4><div><br></div><div>The application also uses TCloudAI to request speech audio from an AI service. When the user selects a place and presses the speak button, the description is sent to the speech service:</div><div><pre class="delphi" name="code">procedure TUILogic.Speak(Description: string);
begin
  FCloudAIAudio.Speak(Description, 1, &#39;nova&#39;);
end;</pre></div><div>The audio request is asynchronous. During initialization, the audio-enabled TCloudAI instance is connected to an event handler:</div><div><pre class="delphi" name="code">FCloudAIAudio := TCloudAI.Create(Self);
FCloudAIAudio.Service := aiOpenAI;
FCloudAIAudio.OnSpeechAudio := SoundReady;

FMP3Player := TStreamMP3Player.Create;</pre></div><div>When the response arrives, the event provides the generated audio as a TMemoryStream:</div><div><pre class="delphi" name="code">procedure TUILogic.SoundReady(
  Sender: TObject;
  HttpStatusCode: Integer;
  HttpResult: string;
  SoundBuffer: TMemoryStream
);
begin
  FMP3Player.Play(SoundBuffer);
end;</pre></div><div>This keeps the network operation and audio playback loosely coupled. TCloudAI is responsible for obtaining the speech, while TStreamMP3Player only needs to know how to play an MP3 stream.<br><br>If you want to see this feature in action, check our video:&nbsp;<a href="https://www.youtube.com/watch?v=VPMEdQxjSd4" target="_blank">https://www.youtube.com/watch?v=VPMEdQxjSd4</a></div><div><br></div><h4>Playing an MP3 stream with FireMonkey</h4><div>Mobile media frameworks generally expect a filename rather than an arbitrary Delphi stream. The TStreamMP3Player class bridges that gap:</div><div><br></div><ol><li>It creates a unique filename using a GUID.</li><li>It writes the stream to TPath.GetCachePath.</li><li>It assigns that temporary file to TMediaPlayer.</li><li>It starts playback.</li><li>It destroys the media player before deleting the temporary file.</li></ol><div><br></div><div>The public API remains small:</div><div><pre class="delphi" name="code">FMP3Player.Play(SoundBuffer);</pre></div><div>The essential implementation is:</div><div><pre class="delphi" name="code">procedure TStreamMP3Player.Play(AStream: TStream);
var
  LFileName: string;
  LFileStream: TFileStream;
begin
  if not Assigned(AStream) then
    raise EArgumentNilException.Create(&#39;AStream&#39;);

  ReleaseMedia;

  LFileName := CreateTempMP3FileName;

  AStream.Position := 0;
  LFileStream := TFileStream.Create(
    LFileName,
    fmCreate or fmShareDenyWrite
  );
  try
    LFileStream.CopyFrom(AStream, 0);
  finally
    LFileStream.Free;
  end;

  FTempFileName := LFileName;
  FMediaPlayer.FileName := FTempFileName;
  FMediaPlayer.CurrentTime := 0;
  FMediaPlayer.Play;
end;
</pre></div><div>One subtle but important detail is the cleanup order. The native media backend may still have an open handle to the MP3 file. Therefore, TMediaPlayer is stopped and destroyed first; only then is the temporary cache file removed. This avoids platform-specific &#147;file in use&#148; problems and makes the class safe to reuse.</div><div><br></div><h4>Conclusion</h4><div>TMSCityGuide illustrates several broadly useful FireMonkey techniques:</div><div><br></div><ul><li>Isolate native platform APIs behind a common Delphi wrapper</li><li>Use conditional compilation inside implementation units rather than throughout the UI</li><li>Use secure sharing mechanisms such as Android FileProvider</li><li>Use temporary and cache directories through System.IOUtils</li><li>Handle asynchronous AI responses with events</li><li>Adapt stream-based data to filename-based platform APIs</li><li>Carefully manage native resources before deleting temporary files.</li></ul><div><br></div><div>The result is application code that remains simple and readable while still integrating deeply with Android, iOS, cloud AI services, and device hardware.</div><div><br></div><div><h4 style="color: rgb(51, 51, 51);">Everything is open source</h4><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;">Like our previous summer projects, <span style="font-weight: 700;">the complete source code is available for free</span>. Clone it, learn from it, extend it, or use parts of it in your own applications.</p><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;">The full project is available on GitHub:</p><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;"><a href="https://github.com/tmssoftware/TMS-City-Guide" style="color: rgb(0, 158, 227); text-decoration: none;"><img src="https://www.tmssoftware.com/site/img/blog/github.png" alt="GitHub repository" style="max-width: 100%; width: 236px; height: 88px;"></a></p><h4 style="color: rgb(51, 51, 51);">Built with TMS components</h4><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;">TMS City Guide showcases how easy it is to combine modern AI capabilities with native cross-platform Delphi development.</p><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;">The application was built using:</p><ul style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;"><li><a href="https://www.tmssoftware.com/site/tmsfncmaps.asp" target="_blank" style="color: rgb(0, 158, 227); text-decoration: none;">TMS FNC Maps</a> for interactive mapping and routing</li><li><a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank" style="color: rgb(0, 158, 227); text-decoration: none;">TMS FNC UI Pack</a> for the modern user interface</li><li><a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank" style="color: rgb(0, 158, 227); text-decoration: none;">TMS AI Studio</a> for AI integration, speech generation and intelligent content</li></ul><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;">To celebrate this summer project, we&#39;re offering a <span style="color: red; font-weight: bold;">25% discount</span> on these products until <span style="font-weight: 700;">August 31, 2026</span>.</p><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;"><img src="https://www.tmssoftware.com/site/img/blog/25-off-discount.png" alt="25% Discount" style="max-width: 100%; width: 137px; height: 137px;"></p><p style="color: rgb(51, 51, 51); font-family: Roboto, sans-serif;">Simply use coupon code&nbsp;<img src="https://www.tmssoftware.com/site/img/blog/couponcode.png" alt="Coupon code" style="max-width: 100%; width: 281px;">&nbsp;during checkout to activate the discount.</p></div>
		
		
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Filtering in Delphi: Building Modern Filter Interfaces]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2507</link>
		<author>Gjalt Vanhouwaert</author>
		<pubDate>Fri, 7 Aug 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		<p>In the <a href="https://tmssoftware.com/site/blog.asp?post=2500">previous blog post</a>, we introduced <b>TTMSFNCFilterView</b> and looked at how users can filter data by interacting with familiar controls instead of building filter expressions manually.</p>

<p>Checkboxes, combo boxes, sliders, date pickers and other controls all contribute to the same structured filter in the background.</p>

<p>Now it is time to see how this works in practice.</p>

<p>In this blog post, we will look at two examples:</p>

<ul>
<li>Using <b>TTMSFNCFilterView</b> together with <b><a href="https://tmssoftware.com/site/tmsfncuipack-grid.asp">TTMSFNCDataGrid</a></b>.</li>
<li>Extending the Filter View with your own controls.</li></ul><p><br></p><ul>
</ul>

<h3>TTMSFNCFilterView and TTMSFNCDataGrid</h3>

<p>The first video demonstrates how <b>TTMSFNCFilterView</b> can be combined with <b><a>TTMSFNCDataGrid</a></b><a> </a>to create a modern filtering interface.</p>

<p style="text-align: center;">
<iframe frameborder="0" src="//www.youtube.com/embed/oeGdDMOIJWI" width="640" height="360" class="note-video-clip"></iframe>
</p>

<p>The Filter View contains the controls the user interacts with, while the DataGrid displays the resulting data.</p>

<p>Whenever the user changes a filter control, the underlying filter expression is updated automatically.</p>

<p>This means you do not have to manually combine all values into one filter string or write separate filtering logic for every control.</p>

<p>In the video, you will see how:</p>

<ul>
<li>Built-in filter controls are added to the Filter View.</li>
<li>Each control is linked to a field and an operator.</li>
<li>Multiple controls contribute to one filter.</li>
<li>The generated filter is applied to <b>TTMSFNCDataGrid</b>.</li>
<li>Different filter actions can be used to determine how matching data is displayed.</li></ul><p><br></p><ul>
</ul>

<h3>Configuring the Filter Controls</h3>

<p>Every control inside the Filter View has an associated filter expression.</p>

<p>This expression determines what should be added to the filter when the user interacts with the control.</p>

<p>For example, a ComboBox can be linked to a country field:</p>

<pre class="delphi" name="code">FilterComboBox.FilterExpression.FilterFieldName := &#39;Country&#39;;
FilterComboBox.FilterExpression.FilterFieldType := fdtText;
FilterComboBox.FilterExpression.ExpressionOperator := feoEqual;</pre>

<p>A CheckBox can represent a Boolean value:</p>

<pre class="delphi" name="code">Active = True</pre>

<p>And a RangeSlider can contribute both a minimum and a maximum value.</p>

<p>Once these expressions are configured, the controls update their part of the filter automatically.</p>

<p>The application only needs to determine what should happen with the resulting filter.</p><p><br></p>

<h3>Applying the Filter</h3>

<p>Because <b>TTMSFNCFilterView</b> uses the same FilterBuilder structure introduced earlier in this series, the resulting filter can be used in different ways.</p>

<p>You can:</p>

<ul>
<li>Apply it to <b>TTMSFNCDataGrid</b>.</li>
<li>Assign the generated filter text to a <code>TDataSet</code>.</li>
<li>Validate individual rows.</li>
<li>Validate a complete array of data.</li>
<li>Use the filter with your own controls or object collections.</li>
</ul>

<p>The Filter View is responsible for building and maintaining the filter, while your application remains in control of how the result is applied.</p><p><br></p>

<h3>Using Your Own Controls</h3>

<p>The built-in controls cover many common filtering scenarios.</p>

<p>However, an application may already contain controls that should become part of the filtering interface.</p>

<p>You may also want to use a control that is specific to your application instead of one of the standard Filter View controls.</p>

<p>The second video demonstrates how your own controls can be integrated into <b>TTMSFNCFilterView</b>.</p>

<p style="text-align: center;">
<iframe frameborder="0" src="//www.youtube.com/embed/enIAg9Lz5gE" width="640" height="360" class="note-video-clip"></iframe>
</p>

<p>This allows you to create a filtering interface that matches the rest of your application without giving up the structured filtering logic.</p><p><br></p>

<h3>Filter View Control Container</h3>

<p>One way to integrate an existing control is by using <b>TTMSFNCFilterViewControlContainer</b>.</p>

<p>The Control Container acts as a bridge between the control and a filter expression.</p>

<p>You configure the expression that the control represents:</p>

<pre class="delphi" name="code">ControlContainer.FilterExpression.FilterFieldName := &#39;Status&#39;;
ControlContainer.FilterExpression.FilterFieldType := fdtText;
ControlContainer.FilterExpression.ExpressionOperator := feoEqual;</pre>

<p>When the control value changes, you update the value stored in the expression.</p>

<pre class="delphi" name="code">ControlContainer.FilterExpression.FilterValue := &#39;Office&#39;;
ControlContainer.FilterExpression.AddToFilter := True;</pre>

<p>If the control represents an option such as <i>All</i>, the expression can simply be excluded from the filter.</p>

<pre class="delphi" name="code">ControlContainer.FilterExpression.AddToFilter := False;</pre>

<p>This gives you control over the user interface while the Filter View continues to manage the complete filter.</p><p><br></p>

<h3>Registering Controls Programmatically</h3>

<p>Controls can also be registered directly with the Filter View.</p>

<p>This is useful when controls are created dynamically or when you want to connect an existing control without placing it inside a Control Container.</p>

<p>For example, a calendar can be linked to a date field:</p>

<pre class="delphi" name="code">TMSFNCFilterView1.AddCustomItemControl(
  FNCCalendar,
  FNCCalendar.SelectedDate,
  &#39;Joined&#39;,
  fdtDate,
  feoSmallerThanOrEqual
);</pre>

<p>When the selected date changes, the linked filter value can be updated:</p>

<pre class="delphi" name="code">procedure TFilterViewForm.FNCCalendarSelectDate(
  Sender: TObject;
  ADate: TDate);
begin
  TMSFNCFilterView1.UpdateCustomItemControlValue(FNCCalendar, ADate);
end;</pre>

<p>The custom control now participates in the same filter as all built-in controls.</p><p><br></p>

<h3>One Filtering Model, Different Interfaces</h3>

<p>The important part is that the FilterBuilder does not need to know whether an expression came from:</p>

<ul>
<li>A built-in Filter View control.</li>
<li>A control inside a Control Container.</li>
<li>A custom control registered programmatically.</li>
<li>A filter dialog.</li>
<li>Code written by the developer.</li>
</ul>

<p>All of these approaches create the same structured filtering model.</p>

<p>This keeps the filtering logic centralized while allowing the user interface to be designed around the requirements of the application.</p><p><br></p>

<h3>Why Build a Custom Filter Interface?</h3>

<p>A predefined filter dialog is useful when users need to create detailed or occasional filters.</p>

<p>A Filter View is more suitable when filtering is a regular part of the application workflow.</p>

<p>Users can immediately see the available filter options and adjust them without leaving the current screen.</p>

<p>By supporting both built-in and custom controls, <b>TTMSFNCFilterView</b> does not force your application into one fixed layout.</p>

<ul>
<li>You can create a webshop-style filter panel.</li>
<li>You can use toolbar buttons for status values.</li>
<li>You can use a calendar for date selection.</li>
<li>You can combine different controls inside filter groups.</li>
<li>You can reuse controls that are already part of your interface.</li>
</ul>

<p>The user interface can be adapted to the application, while all controls continue to contribute to one consistent filter.</p><p><br></p>

<h3>Less Repetitive Filtering Code</h3>

<p>Without a shared filtering component, every control would usually require its own event handling and filtering implementation.</p>

<p>The selected values would then need to be combined manually before applying the filter.</p>

<p>With the Filter View, each control only defines the expression it represents.</p>

<p>The Filter View and FilterBuilder take care of combining these expressions, adding the required groups and generating the final filter.</p>

<p>This reduces the amount of repetitive filtering code and makes it easier to add or change filter controls later.</p><p><br></p>

<h3>What&#39;s Next?</h3>

<p>So far, we have used filter expressions to determine which data matches and which data should be displayed.</p>

<p>In the next blog post, we will look at how the same filtering logic can be used to trigger other behavior.</p>

<p>With <b>TTMSFNCFilterRulesManager</b>, matching a filter can change properties, control visibility, adjust appearance or trigger application logic.</p>

		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿From Delphi Code to City: Our TMS AI City Guide Explores Bruges]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2505</link>
		<author>Bruno Fierens</author>
		<pubDate>Thu, 6 Aug 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		
		<p><img src="https://www.tmssoftware.com/site/img/blog/SummerProject2026/tms-summer-2026-header.jpg" style="width: 100%;" alt="TMS Software Delphi Components tmsaistudio"></p>

<p>Last week we launched our <b>free Summer Project 2026</b>: <b>TMS City Guide</b>, an AI-powered virtual tour guide built entirely with Delphi.</p>

<p>Instead of following a predefined route, TMS City Guide creates a <b>personalized walking tour</b> based on your location and interests. As you walk, the app introduces you to landmarks, hidden gems and historical locations, complete with AI-generated spoken descriptions directly from your smartphone.</p>

<p>If you missed the original announcement, you can read all about the project in <a href="https://www.tmssoftware.com/site/blog.asp?post=2504" target="_blank">our previous blog post</a>.</p>

<h4>See it in action</h4>

<p>A demo is worth a thousand screenshots, so we took TMS City Guide for a real-world test in the beautiful city of <a href="https://www.visitbruges.be/en" target="_blank">Bruges</a>.</p>

<p>Watch how the app generates a walking tour on the fly, navigates between points of interest and brings each location to life with AI-generated narration.</p>

<p style="text-align: center; "><iframe frameborder="0" src="//www.youtube.com/embed/VPMEdQxjSd4" width="640" height="360" class="note-video-clip"></iframe><br></p>

<h4>Everything is open source</h4>

<p>Like our previous summer projects, <b>the complete source code is available for free</b>. Clone it, learn from it, extend it, or use parts of it in your own applications.</p>

<p>The full project is available on GitHub:</p>

<p><a href="https://github.com/tmssoftware/TMS-City-Guide"><img src="https://www.tmssoftware.com/site/img/blog/github.png" style="width:236px;height:88px;" alt="GitHub repository"></a></p>

<h4>Built with TMS components</h4>

<p>TMS City Guide showcases how easy it is to combine modern AI capabilities with native cross-platform Delphi development.</p>

<p>The application was built using:</p>

<ul>
<li><a href="https://www.tmssoftware.com/site/tmsfncmaps.asp" target="_blank">TMS FNC Maps</a> for interactive mapping and routing</li>
<li><a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a> for the modern user interface</li>
<li><a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank">TMS AI Studio</a> for AI integration, speech generation and intelligent content</li>
</ul>

<p>To celebrate this summer project, we&#39;re offering a <span style="color:red;font-weight:bold;">25% discount</span> on these products until <b>August 31, 2026</b>.</p>

<p><img src="https://www.tmssoftware.com/site/img/blog/25-off-discount.png" style="width:137px;height:137px;" alt="25% Discount"></p>

<p>Simply use coupon code <img src="https://www.tmssoftware.com/site/img/blog/couponcode.png" style="width:281px;" alt="Coupon code"> during checkout to activate the discount.</p>

<h4>Coming next: Behind the scenes</h4>

<p>The application contains several interesting Delphi techniques that deserve a closer look.</p>

<p>In an upcoming blog post we&#39;ll explore how we implemented:</p>

<ul>
<li>Exporting generated walking tours as GPX files using the native operating system Share Sheet</li>
<li>Generating and streaming AI-produced speech on the fly</li>
<li>Bringing together AI, mapping and native FireMonkey development into one cross-platform application</li>
</ul>

<p>If you&#39;re interested in the technical implementation, stay tuned&#151;we&#39;ll dive into the source code soon.</p>

<h4>Build it. Customize it. Share it.</h4>

<p>You can clone the project from GitHub and build it yourself for iOS or Android using Delphi. If you&#39;d rather get started immediately, a ready-to-use Android APK is also available in the repository.</p>

<p>To run the application you&#39;ll need:</p>

<ul>
<li>an <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI API key</a></li>
<li>a <a href="https://console.cloud.google.com/apis/dashboard" target="_blank">Google Maps API key</a></li>
<li><a href="https://www.embarcadero.com/products/delphi/starter" target="_blank">Delphi</a></li>
</ul>

<p>Once you&#39;ve created your own walking tours, we&#39;d love to see them! Share the cities you&#39;ve explored, the routes you&#39;ve generated, or the improvements you&#39;ve added. Who knows&#151;your ideas might inspire the next version of TMS City Guide.</p>

<p>Happy coding&#151;and happy exploring! 🌍🚶‍♂️</p>

		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿One Binary, No Apache: The New Sparkle Socket Server for Linux]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2506</link>
		<author>Wagner Landgraf</author>
		<pubDate>Tue, 4 Aug 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		<p>Deploying a Delphi HTTP backend on Linux has always involved a decision you didn&#39;t really want to make.</p>

<p>You could build an <a href="https://httpd.apache.org/">Apache</a> module with WebBroker &#151; which means installing and configuring Apache on every server, matching module ABIs, and debugging a stack where your code is a guest in someone else&#39;s process. Or you could use the <a href="https://www.indyproject.org/">Indy</a>-based server &#151; self-contained, but with no built-in HTTPS (you need a third-party IOHandler), no WebSockets, and modest throughput.</p>

<p>Meanwhile, on Windows, Sparkle servers have had a first-class, kernel-mode HTTP server for a decade.</p>

<p>That gap is now closed. The next version of <strong>TMS Sparkle</strong> introduces the <strong>socket server</strong>: a complete, high-performance HTTP server written entirely in Delphi, on raw sockets. Your Linux deployment becomes a single executable that you copy to the machine and run. No Apache. No <a href="https://nginx.org/">nginx</a>. No third-party libraries to install and keep updated.</p><p><br></p>

<h3>A quick refresher on TMS Sparkle</h3>

<p><strong><a href="https://www.tmssoftware.com/site/sparkle.asp">TMS Sparkle</a></strong> is a high-performance HTTP server framework for Delphi. It&#39;s the foundation under <strong><a href="https://www.tmssoftware.com/site/xdata.asp">TMS XData</a></strong> (REST APIs), <strong><a href="https://www.tmssoftware.com/site/sphinx.asp">TMS Sphinx</a></strong> (<a href="https://oauth.net/2/">OAuth 2.0</a> / <a href="https://openid.net/connect/">OpenID Connect</a>) and <strong><a href="https://www.tmssoftware.com/site/remotedb.asp">TMS RemoteDB</a></strong> (remote database access over HTTP).</p>

<p>Sparkle separates <em>what</em> your server does &#151; <a href="https://doc.tmssoftware.com/biz/sparkle/guide/modules.html">modules</a>, <a href="https://doc.tmssoftware.com/biz/sparkle/guide/middleware.html">middleware</a>, request handlers &#151; from <em>how</em> requests reach it. That "how" is the <a href="https://doc.tmssoftware.com/biz/sparkle/guide/server.html">dispatcher</a>, and until now on Linux your dispatcher choices were Apache or Indy. The socket server is a new dispatcher, and because it implements the same module contract as all the others, <strong>existing Sparkle, XData and RemoteDB servers run on it unchanged</strong>.</p><p><br></p>

<h3>Hello, world</h3>

<p>The server class is <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/index.html"><code>TSocketHttpServer</code></a>, in unit <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/index.html"><code>Sparkle.Socket.Server</code></a>. If you&#39;ve ever written a Sparkle server for Windows with <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.HttpSys.Server/THttpSysServer/index.html"><code>THttpSysServer</code></a>, this will look extremely familiar:</p>

<pre><code>uses
  Sparkle.Socket.Server;

var
  Server: TSocketHttpServer;
begin
  Server := TSocketHttpServer.Create;
  try
    Server.AddModule(TMyServerModule.Create(&#39;http://localhost:8080/myapplication&#39;));
    Server.Start;

    WriteLn(&#39;Server running. Press ENTER to stop.&#39;);
    ReadLn;

    Server.Stop;
  finally
    Server.Free;
  end;
end;
</code></pre>

<p>That&#39;s a complete HTTP server. Compile it for Linux 64-bit, copy the binary to the machine, run it &#151; done.</p>

<p><a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/Stop.html"><code>Stop</code></a> performs a graceful shutdown: listeners close, requests in flight are allowed to finish, and active WebSocket sessions are terminated cleanly.</p>

<blockquote>
<p><strong>A note on the host name.</strong> The socket server uses only the <strong>port</strong> of each module base URL to decide what to listen on &#151; it always binds all interfaces (IPv4 and IPv6) &#151; and routes requests to modules <strong>by path only</strong>. The host part is ignored, so <code>localhost</code>, <code>0.0.0.0</code> or your real domain name all behave the same. That matches the Http.Sys dispatcher, which by default replaces the host with the <code>+</code> wildcard as well (unless you set <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.HttpSys.Server/TCustomHttpSysServer/KeepHostInUrlPrefixes.html"><code>KeepHostInUrlPrefixes</code></a>). In other words, the same base URL works on both dispatchers &#151; and if you need to restrict who can reach the server, that&#39;s a job for a firewall rule or a reverse proxy.</p>
</blockquote>

<p>The same applies to an XData REST API, which needs no changes at all beyond the dispatcher you host it in:</p>

<pre><code>uses
  XData.Server.Module,
  Sparkle.Socket.Server;

  Server := TSocketHttpServer.Create;
  Server.AddModule(TXDataServerModule.Create(
    &#39;http://localhost:2001/tms/xdata&#39;, ConnectionPool));
  Server.Start;
</code></pre>

<h3><br></h3><h3>HTTPS that fits how Linux actually works</h3>

<p>On Linux, certificates are PEM files on disk, usually renewed automatically by <a href="https://certbot.eff.org/">certbot</a>. The socket server works exactly that way &#151; register modules with an <code>https</code> URL and point the <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/Ssl.html"><code>Ssl</code></a> property (<a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Tls/TSslOptions/index.html"><code>TSslOptions</code></a>) at your files:</p>

<pre><code>  Server := TSocketHttpServer.Create;
  Server.Ssl.CertificateFile := &#39;/etc/letsencrypt/live/myapp.com/fullchain.pem&#39;;
  Server.Ssl.PrivateKeyFile := &#39;/etc/letsencrypt/live/myapp.com/privkey.pem&#39;;
  Server.AddModule(TMyServerModule.Create(&#39;https://myapp.com:443/myapplication&#39;));
  Server.Start;
</code></pre>

<p>TLS is provided by <a href="https://www.openssl.org/"><strong>OpenSSL 3</strong></a>, loaded dynamically at runtime. On current distributions (<a href="https://ubuntu.com/">Ubuntu</a> 22.04+, <a href="https://www.debian.org/">Debian</a> 12+, RHEL 9+) OpenSSL 3 is already installed, so there is nothing extra to deploy with your application. <a href="https://datatracker.ietf.org/doc/html/rfc5246">TLS 1.2</a> and <a href="https://datatracker.ietf.org/doc/html/rfc8446">TLS 1.3</a> are both supported.</p>

<p>Two features matter a lot in production:</p>

<p><strong>Several certificates on one port (<a href="https://en.wikipedia.org/wiki/Server_Name_Indication">SNI</a>).</strong> Serve multiple host names from one server, with the right certificate picked from the name the client asks for. Add <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Tls/TSslCertificate/index.html"><code>TSslCertificate</code></a> entries to <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Tls/TSslOptions/SniCertificates.html"><code>SniCertificates</code></a>; wildcards are supported:</p>

<pre><code>var
  Cert: TSslCertificate;
begin
  Cert := TSslCertificate.Create;
  Cert.HostName := &#39;*.example.com&#39;;
  Cert.CertificateFile := &#39;/etc/myserver/example-com.pem&#39;;
  Cert.PrivateKeyFile := &#39;/etc/myserver/example-com.key&#39;;
  Server.Ssl.SniCertificates.Add(Cert);
end;
</code></pre>

<p><strong>Certificate renewal without downtime.</strong> <a href="https://letsencrypt.org/">Let&#39;s Encrypt</a> certificates expire every 90 days. Restarting your server to pick up a renewal means dropping connections. Instead, call <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/ReloadCertificates.html"><code>ReloadCertificates</code></a> &#151; the files are re-read and applied to new connections, while listeners and established connections keep running:</p>

<pre><code>  // e.g. from a certbot deploy hook, or a nightly timer
  Server.ReloadCertificates;
</code></pre>

<p>If the new files can&#39;t be loaded, an exception is raised and the current certificates stay in use &#151; a bad renewal can&#39;t take your server down.</p><p><br></p>

<h3>WebSockets on Linux &#151; finally</h3>

<p>Sparkle has supported <a href="https://doc.tmssoftware.com/biz/sparkle/guide/websockets.html">WebSockets</a> since 2024, but only on Windows. The socket server brings them to Linux, using the exact same API and <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Middleware.WebSocket/TWebSocketMiddleware/index.html"><code>TWebSocketMiddleware</code></a>, over both <code>ws</code> and <code>wss</code>:</p>

<pre><code>type
  TWsEchoModule = class(THttpServerModule)
  public
    constructor Create(const ABaseUri: string); override;
    procedure ProcessRequest(const C: THttpServerContext); override;
  end;

constructor TWsEchoModule.Create(const ABaseUri: string);
begin
  inherited Create(ABaseUri);
  AddMiddleware(TWebSocketMiddleware.Create);
end;

procedure TWsEchoModule.ProcessRequest(const C: THttpServerContext);
var
  Upgrader: IWebSocketUpgrader;
  WebSocket: IWebSocket;
  Msg: IWebSocketMessage;
begin
  Upgrader := C.Item&lt;IWebSocketUpgrader&gt;;
  if Upgrader = nil then
  begin
    C.Response.StatusCode := 400; // not a WebSocket upgrade request
    Exit;
  end;

  WebSocket := Upgrader.Upgrade;
  repeat
    Msg := WebSocket.Receive;
    case Msg.MessageType of
      TWebSocketMessageType.Text:
        WebSocket.Send(&#39;echo: &#39; + Msg.Text);
      TWebSocketMessageType.Close:
        begin
          WebSocket.SendClose(WebSocketStatusCodes.NormalClosure);
          Break;
        end;
    end;
  until False;
end;
</code></pre>

<p>Idle WebSocket sessions are exempt from the read and keep-alive timeouts, so long-lived push connections just work.</p><p><br></p>

<h3>Static files in the same binary</h3>

<p>A REST API usually ships with something to serve: an SPA, a few images, a landing page. Use the standard <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Module.Static/TStaticModule/index.html"><code>TStaticModule</code></a> and it all lives in one process:</p>

<pre><code>uses
  Sparkle.Module.Static;

  Server.AddModule(TStaticModule.Create(&#39;http://localhost:8080/&#39;, &#39;/var/www/myapp&#39;));
</code></pre>

<p>Routing is by path, exactly as in every other Sparkle server &#151; so your API on <code>/tms/xdata</code>, a WebSocket endpoint on <code>/ws</code>, and static files on <code>/</code> coexist happily on one port.</p><p><br></p>

<h3>Handlers that wait &#151; sized automatically</h3>

<p>Here&#39;s the part that usually bites people who write their own server, and the reason a naive thread pool is not enough.</p>

<p>A worker thread is busy for as long as your handler runs. Handlers that return immediately need very few threads. But a handler that <strong>waits</strong> &#151; for a database query, for an API call to another service &#151; holds its worker while doing nothing at all. With a fixed pool, your throughput is capped at <em>workers &#247; handler duration</em>, no matter how idle the machine is. Sixteen threads and a 200 ms query means 80 requests per second, on a machine that&#39;s 95% idle.</p>

<p>The socket server sizes its pool on its own:</p>

<ul>
  <li>It starts with <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/WorkerThreads.html"><code>WorkerThreads</code></a> threads (default: twice the processor count).</li>
  <li>Whenever requests are waiting for a worker, it adds threads, up to <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/MaxWorkerThreads.html"><code>MaxWorkerThreads</code></a> (default: 256).</li>
  <li>Threads above the minimum exit after 30 seconds without work, so the pool shrinks back when the load drops.</li>
  <li>Requests that are handled immediately never grow the pool &#151; a CPU-bound server keeps running on its initial threads.</li>
</ul>

<p><strong>In most cases the defaults are simply correct and there is nothing to configure.</strong> You set these properties when you want a different shape &#151; most commonly to put a ceiling on threads that compete for a limited resource:</p>

<pre><code>  // A database-bound API against a pool of 50 connections:
  // more workers than that would only queue up waiting for a connection.
  Server.WorkerThreads := 16;
  Server.MaxWorkerThreads := 50;
</code></pre>

<h3><br></h3><h3>Built to face the internet</h3>

<p>The socket server is designed to be exposed directly, without a reverse proxy in front of it, so the defensive parts are built in rather than left to nginx:</p>

<ul>
  <li><strong><a href="https://en.wikipedia.org/wiki/Slowloris_(cyber_attack)">Slowloris</a> protection</strong> &#151; <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/HeaderReadTimeout.html"><code>HeaderReadTimeout</code></a> bounds how long a client may take to send its request head (and the TLS handshake). Slow clients never occupy a worker thread: the event loop reads request heads, and only complete requests are handed to a worker.</li>
  <li><strong>Overload protection</strong> &#151; <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/MaxConnections.html"><code>MaxConnections</code></a> caps simultaneous connections and <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/MaxQueuedRequests.html"><code>MaxQueuedRequests</code></a> caps how many requests may wait for a free worker. Beyond that, the server answers <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503"><strong>503</strong></a> and closes, rather than accumulating unbounded latency.</li>
  <li><strong>Size limits</strong> &#151; the <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.Server/TSocketHttpServer/Limits.html"><code>Limits</code></a> property (<a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Socket.HttpParser/TSocketServerLimits/index.html"><code>TSocketServerLimits</code></a>) bounds the request line, individual headers, header count, total head size and body size, with the correct status code for each violation (413, 414, 431, 400).</li>
</ul>

<pre><code>  Server.Limits.MaxBodySize := 50 * 1024 * 1024; // reject bodies over 50 MB
  Server.KeepAliveTimeout := 30000;
</code></pre>

<p>You can still put nginx or <a href="https://caddyserver.com/">Caddy</a> in front if you want <a href="https://datatracker.ietf.org/doc/html/rfc9113">HTTP/2</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9114">HTTP/3</a>, rate limiting or centralized certificates &#151; just add the <a href="https://doc.tmssoftware.com/biz/sparkle/guide/middleware.html#forward-middleware">forward middleware</a> so your handlers see the original client address and scheme.</p><p><br></p>

<h3>The RAD way</h3>

<p>If you build your servers by dropping components on a data module, there&#39;s a new dispatcher component: <strong><a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Comp.SocketDispatcher/TSparkleSocketDispatcher/index.html"><code>TSparkleSocketDispatcher</code></a></strong>. Connect your <code>TXDataServer</code>, <a href="https://doc.tmssoftware.com/biz/sparkle/guide/components.html"><code>TSparkleStaticServer</code></a> or other server components to it through their <code>Dispatcher</code> property, set <code>Active</code> to <code>True</code>, and you&#39;re running.</p>

<p>The underlying server object is available through <a href="https://doc.tmssoftware.com/biz/sparkle/api/Sparkle.Comp.SocketDispatcher/TSparkleSocketDispatcher/Server.html"><code>Server</code></a> for anything you need to configure from code:</p>

<pre><code>uses
  Sparkle.Comp.SocketDispatcher;

  SparkleSocketDispatcher1.Server.Ssl.CertificateFile := &#39;/etc/myserver/cert.pem&#39;;
  SparkleSocketDispatcher1.Server.Ssl.PrivateKeyFile := &#39;/etc/myserver/key.pem&#39;;
  SparkleSocketDispatcher1.Active := True;
</code></pre>

<h3><br></h3><h3>Deploying: a binary and a systemd unit</h3>

<p>Deployment is about as simple as it gets. Compile for Linux 64-bit, copy the binary, and register it as a <a href="https://systemd.io/">systemd</a> service in <code>/etc/systemd/system/myserver.service</code>:</p>

<pre><code>[Unit]
Description=My Sparkle server
After=network.target

[Service]
ExecStart=/opt/myserver/myserver
Restart=always
User=myserver
Group=myserver

[Install]
WantedBy=multi-user.target
</code></pre>

<pre><code>sudo systemctl daemon-reload
sudo systemctl enable --now myserver
</code></pre>

<p>Binding to ports below 1024 needs privileges &#151; either run as root, or grant just the one <a href="https://man7.org/linux/man-pages/man7/capabilities.7.html">capability</a> the binary needs:</p>

<pre><code>sudo setcap cap_net_bind_service=+ep /opt/myserver/myserver
</code></pre>

<h3><br></h3><h3>How fast is it?</h3>

<p>Fast enough that the network is usually the interesting part, not the server.</p>

<p>Measured on a 4-vCPU Ubuntu 24.04 VM with <a href="https://github.com/wg/wrk"><code>wrk</code></a>, 30-second runs (as always with benchmarks: your hardware, handlers and payloads will differ):</p>

<div style="max-width:100%; overflow-x:auto;">
  <table style="width:100%; min-width:620px; border-collapse:collapse;">
    <thead>
      <tr>
        <th style="padding:6px 12px; text-align:left;">Scenario</th>
        <th style="padding:6px 12px; text-align:left; white-space:nowrap;">Socket server</th>
        <th style="padding:6px 12px; text-align:left; white-space:nowrap;">Indy-based server</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="padding:6px 12px;">Small response, 256 connections</td>
        <td style="padding:6px 12px; white-space:nowrap;"><strong>127,127 req/s</strong></td>
        <td style="padding:6px 12px; white-space:nowrap;">30,806 req/s</td>
      </tr>
      <tr>
        <td style="padding:6px 12px;">Chunked 64 KB responses</td>
        <td style="padding:6px 12px; white-space:nowrap;"><strong>67,438 req/s</strong></td>
        <td style="padding:6px 12px; white-space:nowrap;">25,006 req/s</td>
      </tr>
      <tr>
        <td style="padding:6px 12px;">100 KB responses</td>
        <td style="padding:6px 12px; white-space:nowrap;"><strong>59,026 req/s</strong></td>
        <td style="padding:6px 12px; white-space:nowrap;">23,211 req/s</td>
      </tr>
    </tbody>
  </table>
</div>
<p><br></p><p>Throughput scales monotonically with concurrency &#151; there is no dip at moderate connection counts, and no connection resets in the stress runs.</p>

<p><strong>A fair-play note on that Indy column:</strong> those are <em>tuned</em> Indy numbers. Indy does not set <a href="https://en.wikipedia.org/wiki/Nagle%27s_algorithm"><code>TCP_NODELAY</code></a> on accepted connections, so with shipping defaults <a href="https://en.wikipedia.org/wiki/Nagle%27s_algorithm">Nagle&#39;s algorithm</a> and the TCP delayed-ACK timer hold small keep-alive responses back by ~40 ms, capping each connection at roughly <strong>24 req/s</strong>. The benchmark disables Nagle per connection so the comparison measures the servers rather than a missing socket option. The socket server sets <code>TCP_NODELAY</code> on every accepted socket itself, so there is nothing to remember and no such trap.</p>

<p>More interesting than the raw numbers is the blocking-handler behaviour, because that&#39;s what a real XData API does all day. With handlers that wait 500 ms and 256 concurrent connections, the socket server served <strong>509 req/s</strong> &#151; against 504 for an equivalent <a href="https://nodejs.org/">Node.js</a> server and 510 for <a href="https://go.dev/">Go</a>, on the same box in the same session. That&#39;s 99&#150;100% of the arithmetic ceiling (<em>concurrency &#247; latency</em>): the elastic worker pool means a Delphi server waiting on a database is not at a disadvantage against the stacks people usually reach for.</p><p><br></p>

<h3>And how stable is it?</h3>

<p>Speed is the easy half. A server that is exposed to the internet has to survive the boring part &#151; days of load, misbehaving clients, and load it cannot possibly serve. The socket server was built and measured with that in mind:</p>

<ul>
  <li><strong>It doesn&#39;t leak.</strong> Through hours of continuous benchmarking, thread count and open file descriptors tracked concurrency exactly and returned to baseline between runs, with resident memory flat at the end of the campaign.</li>
  <li><strong>No errors where it counts.</strong> Every database-shaped scenario &#151; handlers waiting 50 ms to 500 ms, at 64 and 256 concurrent connections &#151; completed with zero errors and zero dropped connections, on both Linux and Windows.</li>
  <li><strong>Predictable at the limit.</strong> When load exceeds what the configured workers can serve, it answers 503 and closes rather than queueing without bound, so an overloaded server stays responsive and recovers as soon as the spike passes. That threshold is yours to size, via <code>MaxWorkerThreads</code> and <code>MaxQueuedRequests</code>.</li>
  <li><strong>Malformed and hostile input is handled by design, not by luck.</strong> The HTTP parser was fuzzed, and every limit violation maps to a defined status code rather than an unhandled exception.</li>
  <li><strong>It runs the same test suite as every other dispatcher.</strong> The TMS BIZ automated tests execute against the socket server on Linux and Windows, so the module, middleware and WebSocket behaviour you rely on is verified on it &#151; not just on the Windows dispatchers.</li>
</ul>

<h3><br></h3><h3>And on Windows, too</h3>

<p>The socket server is cross-platform: it runs on Windows 32-bit and 64-bit as well, with the same code, the same properties and the same behaviour. (Internally it uses <a href="https://man7.org/linux/man-pages/man7/epoll.7.html">epoll</a> on Linux and <a href="https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll">WSAPoll</a> on Windows, which is about the only thing that differs.)</p>

<p>On Windows the <a href="https://doc.tmssoftware.com/biz/sparkle/guide/server.html#httpsys-based-server">Http.Sys-based server</a> remains the recommended choice for high-load production servers &#151; it&#39;s kernel-mode and integrates with the Windows certificate store. Reach for the socket server on Windows when:</p>

<ul>
  <li>your application <strong>can&#39;t run administrative setup</strong>. Http.Sys needs <code>netsh</code> URL reservations and certificate bindings; the socket server binds plain sockets and needs none, so it runs under any user account.</li>
  <li>you want <strong>identical behaviour on both platforms</strong> &#151; develop on Windows, deploy on Linux, with one configuration and no surprises.</li>
  <li>you prefer <strong>PEM certificates with SNI and hot reload</strong> over the Windows certificate store.</li>
</ul>

<h3><br></h3><h3>One more thing for Linux</h3>

<p>A related fix in the same release removes another long-standing Linux annoyance: Sparkle now registers <strong><a href="https://datatracker.ietf.org/wg/jose/about/">JOSE</a> cryptography providers backed by OpenSSL 3</strong>, so <a href="https://jwt.io/">JWT</a> tokens signed with <a href="https://en.wikipedia.org/wiki/RSA_cryptosystem">RSA</a> (RS256/384/512) and <a href="https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm">ECDSA</a> (ES256/384/512) work on distributions that no longer ship the legacy OpenSSL 1.x libraries &#151; Ubuntu 22.04 and later, among others. This is what lets a <a href="https://www.tmssoftware.com/site/sphinx.asp">TMS Sphinx</a> server using RSA-signed tokens run on a modern Linux box. The providers are registered automatically and fall back to the previous implementation when OpenSSL 3 isn&#39;t available, so existing applications need no changes.</p><p><br></p>

<h3>Conclusion</h3>

<p>The socket server makes Linux a first-class deployment target for Delphi HTTP servers:</p>

<ul>
  <li><strong>One self-contained binary</strong> &#151; no Apache, no nginx, no third-party libraries to install or keep updated.</li>
  <li><strong>Everything a public-facing server needs</strong> &#151; HTTPS with SNI and zero-downtime certificate reload, WebSockets, static files, keep-alive, chunked encoding, and built-in protection against oversized, slow and excessive requests.</li>
  <li><strong>Fast, and steady under load</strong> &#151; including the case that matters most, handlers that wait on a database.</li>
  <li><strong>Nothing to rewrite</strong> &#151; existing Sparkle, XData and RemoteDB modules run on it as they are.</li>
  <li><strong>Cross-platform</strong> &#151; the same server, the same configuration, on Linux and Windows.</li>
</ul>

<p>It is now the recommended way to deploy <a href="https://www.tmssoftware.com/site/sparkle.asp">TMS Sparkle</a>, <a href="https://www.tmssoftware.com/site/xdata.asp">TMS XData</a> and <a href="https://www.tmssoftware.com/site/remotedb.asp">TMS RemoteDB</a> servers on Linux.</p>

<p>A complete demo &#151; dynamic endpoint, static files, WebSocket echo and optional HTTPS, in a single program that builds for both platforms &#151; ships in the Sparkle distribution under <code>demos/SocketServer</code>.</p>

<p>To learn more, read the full <strong><a href="https://doc.tmssoftware.com/biz/sparkle/guide/socket-server.html">Socket Server documentation</a></strong> and the <strong><a href="https://doc.tmssoftware.com/biz/sparkle/about/whats-new.html">what&#39;s new list</a></strong>, and visit the <strong><a href="https://www.tmssoftware.com/site/sparkle.asp">TMS Sparkle product page</a></strong>.</p>

		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Build Your Own AI-Powered Summer City Guide in Delphi]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2504</link>
		<author>Bruno Fierens</author>
		<pubDate>Thu, 30 Jul 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		<p><img src="https://www.tmssoftware.com/site/img/blog/SummerProject2026/tms-summer-2026-header.jpg" style="width: 100%;" alt="TMS Software Delphi Components tmsaistudio"></p><h4>Build a Summer City Walking Tour Guide with <a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank">TMS AI Studio</a>, <a href="https://www.tmssoftware.com/site/tmsfncmaps.asp" target="_blank">TMS FNC Maps</a> and <a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a></h4><div><br></div><div>Summer is the ideal moment to explore a city by walking. Whether you are visiting a new destination, rediscovering your own city, or looking for a pleasant afternoon walk with a bit of culture, food, architecture and nature, a good walking tour can make the difference between wandering around and really experiencing a place.<br><br>And summer time is also an ideal time to get a discount for the products used in this Summer project! Take this opportunity to get <font style="color:red"><b>25% discount</b></font> on <a href="https://www.tmssoftware.com/site/tmsfncmaps.asp" target="_blank">TMS FNC Maps</a>, <a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a>, <a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank">TMS AI Studio</a> or <a href="https://www.tmssoftware.com/site/tmsfncstudio.asp" target="_blank">TMS FNC Component Studio</a>. The coupon code is valid till Aug 31, 2026.<br><br></div><div><img src="https://www.tmssoftware.com/site/img/blog/25-off-discount.png" style="width: 137.234px; height: 137.234px;" alt="TMS Software Delphi  Components tmsfncmaps">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Use coupon code :<img src="https://www.tmssoftware.com/site/img/blog/couponcode.png" style="width: 281px;" alt="TMS Software Delphi  Components tmsfncmaps">on the online order form to activate the discount!&nbsp;<br><br></div><div><br></div><div>With the <b>Summer 2026 Delphi project</b>, we wanted to show how quickly such an experience can be built with 3 TMS products working together, an <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI API key </a>and <a href="https://console.cloud.google.com/apis/dashboard" target="_blank">Google Maps API key</a> and of course <a href="https://www.embarcadero.com/products/delphi/starter" target="_blank">Delphi</a>:</div><div><br></div><ul><li><a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank">TMS AI Studio</a> for AI-generated tour content and speech</li><li><a href="https://www.tmssoftware.com/site/tmsfncmaps.asp" target="_blank">TMS FNC Maps</a> for geocoding, directions, markers, route drawing and GPX export</li><li><a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a> for the polished cross-platform user interface</li></ul><div><br></div><div>The result is a mobile-friendly FireMonkey application that <b>turns a city name and a few selected interests automatically into an AI-generated walking guide</b>.</div><div><br></div><div>And to make it even easier to explore the project yourself, the full source code of this summer project is available from our <a href="https://github.com/tmssoftware/TMS-City-Guide" target="_blank">GitHub repository</a>.</div><div><br></div><div><br></div><h4>From a City Name to a Personalized Walking Tour</h4><div><br></div><div>The idea behind the application is simple.</div><div><br></div><div>The user enters a city (or the city is retrieved from the device location), selects one or more categories of interest, and taps <b>"Generate my tour"</b>. The available categories cover typical recreational city-trip interests such as:</div><div><br></div><ul><li>Museums</li><li>Historical Places</li><li>Architecture</li><li>Parks &amp; Nature</li><li>Entertainment</li><li>Shopping</li><li>Food &amp; Drinks</li><li>Art &amp; Galleries</li></ul><div><br></div><div>From there, the application asks the AI (OpenAI ChatGPT in this case) to find a good set of points of interest matching the selected categories and to order them as a pleasant walking tour of approximately 5 km by default or the distance of your choice.&nbsp;</div><div><br></div><div><img src="https://www.tmssoftware.com/site/img/blog/summer2026_1.png" style="width: 323px;" alt="TMS Software Delphi  Components tmsfncmaps">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;<img src="https://www.tmssoftware.com/site/img/blog/summer2026_2.png" style="width: 323px;" alt="TMS Software Delphi  Components tmsfncmaps">&nbsp;<br><br><i>Screen for entering city or retrieve current location&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; The visualization of the calculated route<br>and categories of places you want to visit<br></i><br><img src="https://www.tmssoftware.com/site/img/blog/summer2026_3.png" style="width: 323px;" alt="TMS Software Delphi  Components tmsfncmaps"></div><div><br><i>The list of places along the walking tour and a description.<br>Click the speaker icon to hear your digital guide explain the POI</i></div><div><br></div><div>This is where <a href="https://www.tmssoftware.com/site/tmsaistudio.asp" target="_blank">TMS AI Studio</a> makes the experience feel natural. Instead of hard-coding a database of places for every possible city, the application can ask the AI to suggest relevant POIs, describe them, classify them by type and provide a related Wiki URL. The generated result is then fed back into the Delphi application as structured data, ready to be displayed in the UI and used by the <a href="https://www.tmssoftware.com/site/tmsfncmaps.asp" target="_blank">TMS FNC Maps</a> mapping components.</div><div><br></div><div>For the user, this is just a few taps. For the developer, it avoids a large amount of manually curated content and opens the door to dynamic, location-aware application scenarios.</div><div><br></div><div><br></div><h4>AI That Calls Back into Your Delphi Code</h4><div><br></div><div>One of the strengths of TMS AI Studio is that it is not limited to a prompt-and-answer workflow. In this demo, the application defines AI tools the model can call, such as adding a tour place to the route list or reporting that no suitable place was found.</div><div><br></div><div>The AI receives the instruction to find POIs for the requested city, category selection and walking distance. For every result, it calls back into Delphi code with:</div><div><br></div><ul><li>The POI name</li><li>A short description</li><li>The POI category</li><li>A Wiki URL</li></ul><p>This is achieved by a tool call that is setup:</p><p></p><pre class="delphi" name="code">var
  p: TCloudAIParameter;<br>begin<br>  CloudAI.Tools.Add;
  CloudAI.Tools[0].Name := &#39;addtourplace&#39;;
  CloudAI.Tools[0].Description := &#39;Add a POI name, description and Wiki Page URL for the walking tour to the list&#39;;
  CloudAI.Tools[0].OnExecute := ToolExecuted;

  p := FCloudAI.Tools[0].Parameters.Add;
  p.Name := &#39;POI&#39;;
  p.Description := &#39;The name of the POI&#39;;
  p.Required := true;
  p.&amp;Type := ptString;

  p := FCloudAI.Tools[0].Parameters.Add;
  p.Name := &#39;Description&#39;;
  p.Description := &#39;A brief summary description of the POI&#39;;
  p.Required := true;
  p.&amp;Type := ptString;

  p := FCloudAI.Tools[0].Parameters.Add;
  p.Name := &#39;Type&#39;;
  p.Description := &#39;The type of the POI from the list &#39; + CategoriesList([low(TPOICategory)..high(TPOICategory)]);
  p.Required := true;
  p.&amp;Type := ptString;

  p := FCloudAI.Tools[0].Parameters.Add;
  p.Name := &#39;WikiURL&#39;;
  p.Description := &#39;The URL of the Wiki page of POI&#39;;
  p.Required := true;
  p.&amp;Type := ptString;<br>end;
</pre><p>This gives the application clean, structured data instead of free-form text that still needs to be parsed afterwards. That is an important difference when building real applications: the AI becomes part of the workflow, while the Delphi application remains in control of how the data is stored, shown and processed.</p><div><br></div><div><br></div><h4>Putting the Route on the Map</h4><div><br></div><div>Once the POIs are available, TMS FNC Maps takes over the geographical part.</div><div><br></div><div>The application uses geocoding to resolve the selected city and each point of interest to coordinates.Experiments have proven that the accuracy of retrieving POI coordinates is still noticeable better with a Google Maps geocoding API call covered by <a href="https://doc.tmssoftware.com/fnc/maps/geocoding/index.html" target="_blank">TTMSFNCGeoCoding</a>&nbsp; than what the LLM can return (if it can already return it). It then uses directions support available in the <a href="https://doc.tmssoftware.com/fnc/maps/api/ttmsfncdirections/index.html" target="_blank">TTMSFNCDirections </a>component to create a walking route between the places. On the map, every POI is shown as a marker with a category-specific icon, and the route is drawn as a polyline.</div><div><pre class="delphi" name="code">// build the waypoints array and ask the directions service to calculate a route 
// from starting place back to starting place with all waypoints in between
var
  i: integer;
  org: TTMSFNCMapsCoordinateRec;
begin
  org.Longitude := Places[0].Coord.Longitude;
  org.Latitude := Places[0].Coord.Latitude;
  SetLength(wp, Places.Count - 1);

  for i := 1 to Places.Count - 1 do
  begin
    wp[i - 1].Longitude := Places[i].Coord.Longitude;
    wp[i - 1].Latitude := Places[i].Coord.Latitude;
  end;

  FDirections.GetDirections(org, org, nil, &#39;&#39;, nil, false, tmWalking, wp, true);<br>end;</pre></div><div>The user can switch between a list view and a map view. In the list, the POIs are shown with names, descriptions and category icons. On the map, the same places are visible in their geographical context, connected as a walkable tour.</div><div><br></div><div>The project also includes GPX export, so the generated city tour can be saved as a walking route. That makes the demo not only visually appealing, but also useful as a basis for real travel, tourism, event or leisure applications. Again, achieved by a single call <a href="https://doc.tmssoftware.com/fnc/maps/api/ttmsfncmaps/savetogpxfile.html" target="_blank">SaveToGPXFile()</a> from the TTMSFNCMaps component.</div><div><br></div><div><br></div><h4>A UI That Feels Like a Mobile Travel Companion</h4><div><br></div><div>For the user interface, the project uses TMS FNC UI Pack components such as panels, toolbar buttons, list boxes, HTML text display, bitmap containers and waiting indicators.</div><div><br></div><div>The app flow is deliberately compact:</div><div><br></div><ul><li>Enter a city</li><li>Pick one or more POI categories</li><li>Generate the walking tour</li><li>Review the generated places</li><li>Open the map</li><li>Tap a marker or list item for more information</li><li>Listen to the description at the POI</li><li>Export the route when needed</li></ul><div><br></div><div>The category selector is implemented as a visual grid of buttons with icons. The overview screen can show either the map or the list. Waiting indicators are used while the AI and route services are doing their work. The settings screen allows API keys and preferences to be entered and stored.</div><div><br></div><div>All of this is built in a Delphi FMX project, making it suitable for desktop testing and mobile deployment with one code-base. The project includes Android and iOS targets, which is especially relevant for a walking guide: the best device for this application is the smartphone in your pocket while you are out in the city.</div><div><br></div><div><br></div><h4>Let the Smartphone Speak at the POI</h4><div><br></div><div>A city guide becomes much more convenient when you do not need to keep reading from the screen.</div><div><br></div><div>When the user selects a POI, the application shows the name and generated description. At that point, the Speak button becomes available. With one tap, t<b>he description is sent through the AI text to speech functionality</b> and played back as audio on the device.</div><div><br></div><div>This turns the application into <b>a lightweight audio guide</b>. You arrive at a museum, park, square or restaurant area, tap the POI and let the smartphone tell you what is interesting about the place.</div><div><br></div><div>For tourism and recreational apps, this is a powerful detail. Text-to-speech makes the application more accessible, more comfortable to use outdoors, and more immersive. From a developer perspective, it is also a good example of how TMS AI Studio can bring multiple AI capabilities into the same Delphi application: first generating the tour, then speaking the information.</div><div><br></div><div><iframe frameborder="0" src="//www.youtube.com/embed/iXvDD3AnTMk" width="640" height="360" class="note-video-clip"></iframe><br></div><div><br></div><h4>Why This Demo Matters</h4><div><br></div><div>The Summer 2026 project is not just a nice seasonal app idea. It demonstrates a practical pattern for modern Delphi development:</div><div><br></div><ul><li>Use AI to generate contextual, personalized content.</li><li>Use mapping components to turn that content into real-world navigation.</li><li>Use a rich cross-platform UI toolkit to make the experience easy and pleasant.</li></ul><div><br></div><div>The same pattern can be reused in many other applications:</div><div><br></div><ul><li>Tourism apps</li><li>Museum and campus guides</li><li>City event applications</li><li>Hotel concierge tools</li><li>Conference venue guides</li><li>Real estate neighborhood explorers</li><li>Outdoor activity planners</li></ul><div><br></div><div>The categories, prompts, route distance and generated content can all be adapted to the domain. The core architecture remains the same: TMS AI Studio provides the intelligence, TMS FNC Maps provides the location and routing layer, and TMS FNC UI Pack provides the user experience.</div><div><br></div><div><br></div><h4>A Productive Combination for Delphi Developers</h4><div><br></div><div>What makes this demo attractive is the speed at which an advanced application concept becomes achievable. Thanks to the powerful components that wrap a lot of the needed functionality, <b>this idea grew into a full-blown app in a couple of days only</b>.</div><div><br></div><div>Together, these products let Delphi developers focus on the actual application idea: in this case, a summer recreational city walking tour guide that is generated on demand, visualized on a map and spoken aloud when the user reaches a place of interest.</div><div><br></div><div>It is a small project with a very recognizable use case, but it shows a much larger opportunity. AI is most valuable when it is connected to real application workflows. Maps are most valuable when they are connected to meaningful content. A good UI is most valuable when it keeps both of those things simple for the user.</div><div><br></div><div>TMS Summer 2026 Project brings all three together in one clear demo.</div><div><br></div><div><br></div><h4>Conclusion &amp; share your city walks!</h4><div><br></div><div>With only a city name and a few chosen interests, the application creates a personalized walking tour, displays it on an interactive map, lists the selected points of interest, exports the route and lets the smartphone speak the POI information aloud. Thanks to the cross-platform power of Delphi, we can create our own app for iOS &amp; Android mobile devices and have it useful for once also in our leisure time!<br><br>Do you live in an interesting city or visit an interesting city, take the app with you, <b>make a walking tour and email the result as GPX file</b>. We can share the nicest walking tours here.&nbsp;<br><br>Without a doubt, you&#39;ll have cool ideas for additional features in the app. Let us know about it or <b>add these features to this open-source app and see it grow</b>!<br><br><br></div><div><br></div><div><br></div><div><br></div>
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Free Database Generator Sample with our Next Generation Data Grid for Delphi]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2503</link>
		<author>Pieter Scheldeman</author>
		<pubDate>Thu, 30 Jul 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		
		
		
		
		<p><span style="background-color: #12233f; color: white; padding: 6px 12px; text-align: center; border-radius: 6px; font-size: 1.35em;">Free sample</span></p>

<h4><br></h4><h4>Why build reports around a DataGrid?</h4>
<p>Many application reports begin in exactly the same way: connect to a database, execute a query, inspect the result, make a few visual decisions, and send the data somewhere else. The destination may be a browser, an API, a PDF archive, a CSV import, or an Excel workbook, but most of the work before that point is identical.</p>

<p>A traditional report designer is invaluable for invoices, labels, and highly structured documents. For operational reports, however, a data grid is often the more natural authoring surface. Users can see the records immediately, sort a column, apply a filter, and export exactly the view that answers their question.</p>

<p>That idea is the basis of the&nbsp;<strong>FNC Database Generator</strong>, a free source-included FireMonkey sample built with <a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a>. It connects a FireDAC dataset to <code>TTMSFNCDataGrid</code> and turns the active renderer into a small multi-format reporting engine.</p>

<table style="width: 100%; border-collapse: separate; border-spacing: 0; margin: 22px 0; border: 1px solid #dce4f0; border-radius: 10px; overflow: hidden;">
  <tbody><tr style="background-color: #12233f; color: white;">
    <th style="padding: 11px; text-align: left;">Stage</th>
    <th style="padding: 11px; text-align: left;">Component</th>
    <th style="padding: 11px; text-align: left;">Responsibility</th>
  </tr>
  <tr><td style="padding: 10px; border-top: 1px solid #dce4f0;"><strong>Connect</strong></td><td style="padding: 10px; border-top: 1px solid #dce4f0;"><code>TFDConnection</code></td><td style="padding: 10px; border-top: 1px solid #dce4f0;">Any installed FireDAC driver and connection string</td></tr>
  <tr style="background-color: #f5f8fd;"><td style="padding: 10px; border-top: 1px solid #dce4f0;"><strong>Query</strong></td><td style="padding: 10px; border-top: 1px solid #dce4f0;"><code>TFDQuery</code></td><td style="padding: 10px; border-top: 1px solid #dce4f0;">The report dataset</td></tr>
  <tr><td style="padding: 10px; border-top: 1px solid #dce4f0;"><strong>Present</strong></td><td style="padding: 10px; border-top: 1px solid #dce4f0;"><code>TTMSFNCDataGrid</code></td><td style="padding: 10px; border-top: 1px solid #dce4f0;">Preview, sort, filter, and visual styling</td></tr>
  <tr style="background-color: #f5f8fd;"><td style="padding: 10px; border-top: 1px solid #dce4f0;"><strong>Export</strong></td><td style="padding: 10px; border-top: 1px solid #dce4f0;">FNC IO classes</td><td style="padding: 10px; border-top: 1px solid #dce4f0;">HTML, JSON, PDF, CSV, XLS, and XLSX</td></tr>
</tbody></table>

<h4><span style="color: inherit;"><br></span></h4><h4><span style="color: inherit;">One adapter turns a dataset into a report view</span></h4>
<p><code>TTMSFNCDataGridDatabaseAdapter</code> connects the dataset to the DataGrid renderer. The important detail is <code>almAllRecords</code>: the report renderer receives the complete result set, not only the records currently needed for the visible viewport.</p>

<pre class="delphi" name="code">Adapter.DataSource := DataSource;
Adapter.Renderer := Grid.Root;
Adapter.AutoCreateColumns := True;
Adapter.LoadMode := almAllRecords;
Adapter.Active := True;</pre>

<p>The grid then adds the interaction that users expect from a professional data tool:</p>

<ul>
  <li><p><strong>Alternating row bands</strong> for easier scanning.</p></li>
  <li><p><strong>Single-row selection</strong> with a restrained highlight.</p></li>
  <li><p><strong>Conditional formatting</strong> that marks overdue orders in red and paid orders in green.</p></li>
  <li><p><strong>Stretching columns</strong> for a clean desktop layout.</p></li>
</ul>

<p><img src="https://www.tmssoftware.com/site/img/FNCDBGenerator/screenshots/app.png" style="display: block; width: 82%; max-width: 1080px; height: auto; margin: 0 auto; border: 1px solid #dce4f0; border-radius: 10px; box-shadow: 0 8px 24px rgba(18,35,63,0.12);" data-filename="app.png" alt="FNC Database Generator FireMonkey application showing connection, query, export toolbar, and DataGrid report"></p>
<p style="color: #718096; font-size: 0.92em; text-align: center;">The FMX application keeps connection, preview, and every export action in one focused workspace.</p><p style="color: #718096; font-size: 0.92em; text-align: center;"><br></p>

<h4>Conditional Formatting</h4><p>In the sample, some rows are highlighted. This is due to conditional formatting based on the data. The code shows how to apply certain rules while working with dynamic data.</p><p></p><pre class="delphi" name="code">procedure TMainForm.GridGetCellLayout(Sender: TObject;
  ACell: TTMSFNCDataGridCell);
var
  SavedRecord: Integer;
  StatusField: TField;
begin
  if (ACell.Row &lt; Grid.FixedRowCount) or not Adapter.CheckDataSet then
    Exit;

  StatusField := Query.FindField(&#39;Status&#39;);
  if not Assigned(StatusField) then
    Exit;

  SavedRecord := Adapter.DataLink.ActiveRecord;
  try
    if Adapter.SetActiveRecord(ACell.Row) then
      if SameText(StatusField.AsString, &#39;Overdue&#39;) then
      begin
        ACell.Layout.Fill.Color := $FFFFEEEE;
        ACell.Layout.Font.Color := COLOR_RED;
      end
      else if SameText(StatusField.AsString, &#39;Paid&#39;) then
        ACell.Layout.Font.Color := COLOR_GREEN;
  finally
    Adapter.DataLink.ActiveRecord := SavedRecord;
  end;
end;</pre><br><p></p><h4>HTML and CSV: native renderer exports</h4>
<p>The DataGrid renderer already knows which rows and columns belong to the active view. HTML and CSV therefore need only one call each. The generated HTML includes CSS classes for the grid appearance, while CSV applies the configured delimiter and quoting rules.</p>

<pre class="delphi" name="code">Grid.SaveToHTMLData(FileName, TEncoding.UTF8);

Grid.Options.IO.Delimiter := &#39;,&#39;;
Grid.Options.IO.QuoteEmptyCells := True;
Grid.SaveToCSVData(FileName, TEncoding.UTF8);</pre>

<p><img src="https://www.tmssoftware.com/site/img/FNCDBGenerator/screenshots/html-export.png" style="display: block; width: 100%; height: 330px; object-fit: cover; object-position: left top; border: 1px solid #dce4f0; border-radius: 10px; box-shadow: 0 8px 24px rgba(18,35,63,0.10);" data-filename="fnc-report-studio-html-export.png" alt="HTML report exported from the FNC DataGrid and rendered in a browser"></p>
<p style="color: #718096; font-size: 0.92em; text-align: center;">The browser-rendered HTML export carries the same columns, banding, and paid/overdue colors.</p>

<h4>JSON: the visible grid as structured data</h4>
<p>JSON is deliberately implemented as a small serializer in the demo. It walks the renderer instead of the original query, skips hidden and filtered rows, and uses the fixed header row as property names. This means the JSON payload describes the same report view that the user sees.</p>

<pre class="delphi" name="code">for Row := Renderer.FixedRowCount to Renderer.RowCount - 1 do
  if not Renderer.IsRowHidden(Row) and
     not Renderer.IsRowFiltered(Row) then
  begin
    RowObject := TJSONObject.Create;
    for Column := 0 to Renderer.ColumnCount - 1 do
      if not Renderer.IsColumnHidden(Column) then
        RowObject.AddPair(
          TTMSFNCDataGridData.ValueToString(Renderer.Cells[Column, 0]),
          TTMSFNCDataGridData.ValueToString(Renderer.Cells[Column, Row]));
    Rows.AddElement(RowObject);
  end;</pre>

<p>The exported document also includes the report name, generation time, and source connection string. In a production application you can replace the last item with a friendly connection name when credentials or host details should not leave the application.</p>

<h4><br></h4><h4>PDF: a report that is ready to share</h4>
<p><code>TTMSFNCDataGridPDFIO</code> handles pagination and draws the DataGrid using the FNC PDF engine. The sample adds a report title, a generated timestamp, page numbers, fit-to-page scaling, and repeated fixed rows.</p>

<pre class="delphi" name="code">PDFIO.DataGrid := Grid;
PDFIO.Options.Header := &#39;SALES PERFORMANCE REPORT&#39;;
PDFIO.Options.Footer := &#39;Generated &#39; +
  FormatDateTime(&#39;dd mmm yyyy, hh:nn&#39;, Now);
PDFIO.Options.PageNumber := pnFooter;
PDFIO.Options.PageNumberFormat := &#39;Page %d&#39;;
PDFIO.Options.FitToPage := True;
PDFIO.Options.RepeatFixedRows := True;
PDFIO.Save(FileName);</pre>

<p>Because the PDF exporter uses the renderer, the output carries across the navy header, banded rows, and conditional colors. Longer result sets automatically continue on additional pages.</p>

<p><img src="https://www.tmssoftware.com/site/img/FNCDBGenerator/report-preview.png" style="width: 100%; height: 520px; object-fit: cover; object-position: top; border: 1px solid #dce4f0; border-radius: 10px; box-shadow: 0 8px 24px rgba(18,35,63,0.10);" data-filename="fnc-report-studio-pdf-export.png" alt="PDF report generated by the built-in FNC DataGrid PDF exporter"></p>
<p style="color: #718096; font-size: 0.92em; text-align: center;">A rendered page from the verified PDF: report title, repeated DataGrid header, styled rows, footer timestamp, and page number.</p>

<h4><br></h4><h4>Excel, in both generations</h4>
<p>The sample treats <code>.xls</code> and <code>.xlsx</code> as two real formats rather than two file extensions for the same binary content.</p>

<ul>
  <li><p><strong>XLS:</strong> <code>TTMSFNCDataGridExcelIO</code> exports the current renderer to the classic Excel binary workbook format and preserves cell properties.</p></li>
  <li><p><strong>XLSX:</strong> the optional <strong>FlexCel</strong> build converts that styled XLS workbook into a genuine modern Office Open XML workbook.</p></li>
</ul>

<pre class="delphi" name="code">ExcelIO.Renderer := Grid.Root;
ExcelIO.DataGridStartRow := 0;
ExcelIO.DataGridStartCol := 0;
ExcelIO.Options.ExportOverwrite := omAlways;
ExcelIO.Options.ExportCellProperties := True;
ExcelIO.XLSExport(IntermediateXLS, &#39;Sales report&#39;);

Workbook := TXlsFile.Create(IntermediateXLS, True);
try
  Workbook.Save(FileName, TFileFormats.Xlsx);
finally
  Workbook.Free;
end;</pre>

<div style="background-color: #fff8e8; border-left: 5px solid #e6a629; padding: 16px 18px; border-radius: 8px; margin: 22px 0;">
  <p style="margin: 0 0 6px 0;"><strong>FlexCel is optional by design</strong></p>
  <p style="margin: 0;">The built-in DataGrid exporter produces the classic XLS format; changing the extension alone does not make it XLSX. Because TMS FlexCel VCL might not be installed on every development machine, the normal <code>Debug</code> and <code>Release</code> configurations contain no direct FlexCel unit references. Choose the dedicated <code>FlexCel</code> configuration only when that product is installed and its units are on Delphi&#39;s search path.</p>
</div>

<p>In a core build the toolbar marks the command <strong>XLSX*</strong>; selecting it gives a clear dependency message instead of producing an XLS file with a misleading extension. A FlexCel build exports an intermediate XLS, converts it to ZIP-based XLSX, and removes the intermediate file. FlexCel is required on the build machine, but users do not install it separately when the finished application is deployed without runtime packages.</p>

<div style="width: 100%; height: 360px; overflow: hidden; border: 1px solid #dce4f0; border-radius: 10px; box-shadow: 0 8px 24px rgba(18,35,63,0.10);">
  <img src="https://www.tmssoftware.com/site/img/FNCDBGenerator/screenshots/xlsx-export.png" style="display: block; width: 155%; max-width: none; height: auto; margin: 0;" data-filename="fnc-report-studio-flexcel-xlsx-export.png" alt="Genuine XLSX workbook generated through FlexCel and opened in Microsoft Excel">
</div>
<p style="color: #718096; font-size: 0.92em; text-align: center;">The FlexCel configuration produces a genuine XLSX workbook, shown here opened in Excel with the complete header, first column, banding, and conditional colors intact.</p>

<h4><br></h4><h4>A UI designed for the workflow</h4>
<p>The FMX form is intentionally more than a row of test buttons. It uses a dark product header, separate connection and SQL cards, a clear export toolbar, a large report canvas, and a persistent status area that reports the record count and output path. The result feels like a small utility rather than a component test.</p>

<p>All generated files are written to an <code>Output</code> folder beside the executable. This keeps the sample database and every generated report together with the active demo build. When deploying to a platform that protects application bundles, place the application in a writable location or redirect this folder to platform-specific application storage.</p>

<div style="background: linear-gradient(135deg, #12233f 0%, #234a88 100%); color: white; padding: 22px; border-radius: 12px; margin: 24px 0; text-align: center;"><p style="font-size: 1.35em; font-weight: bold; margin: 0 0 8px 0;">Try the FNC Database Generator sample</p>
  <p style="color: #d9e5fa; margin: 0 0 16px 0;">Download the free FMX source sample and open it directly in Delphi.</p>
  <p style="margin: 0;"><span style="display: inline-block; padding: 11px 20px; border-radius: 7px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; background-color: rgb(255, 255, 255);"><b style=""><a href="https://www.tmssoftware.com/download/samples/FNCDBReportGenerator.zip" target="_blank" style="">Download</a></b></span></p>
</div>

<p>Natural next steps include saved report definitions, query parameters, a column chooser, chart summaries, scheduled exports, or cloud upload. The important architectural point stays the same: the DataGrid renderer is the reusable report model, while each output format is a focused adapter around it.</p>

<p>If TMS FNC is installed through Smart Setup, the product family can be installed from the command line:</p>

<pre class="delphi" name="code">tms self-update
tms install tms.fnc.*</pre>

<h4><br></h4><h4>Conclusion</h4>
<p>The FNC Database Generator sample shows how far a database-aware DataGrid can go beyond on-screen browsing. One FireDAC query becomes an interactive report, and one renderer becomes HTML, JSON, PDF, CSV, XLS, or XLSX.</p>

<p>For operational reporting, internal tools, exports, and data handoffs, this approach stays pleasantly direct: query the data, make the grid look right, and let the current view become the report.</p>

		
		
		
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Filtering in Delphi: Let Users Filter Naturally]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2500</link>
		<author>Gjalt Vanhouwaert</author>
		<pubDate>Fri, 24 Jul 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		<p>In <a href="https://tmssoftware.com/site/blog.asp?post=2499">the previous blog post</a>, we showed you how to use the <b>TTMSFNCFilterDialog</b>, allowing users to build complex filter expressions through a guided interface.</p>

<p>That works well when users intentionally want to create or modify a filter.</p>

<p>However, many modern applications don&#39;t expose filtering as a separate action anymore.</p>

<p>Think about a webshop.</p>

<p>You don&#39;t open a filter editor or write filter expressions.</p>

<p>You simply select a category, choose a few brands, move a price slider and perhaps select a delivery date.</p>

<p>The results update automatically while you interact with the page.</p>

<p>That is exactly the idea behind <b>TTMSFNCFilterView</b>.</p><p style="text-align: center; "><img src="https://www.tmssoftware.com/site/img/blog/FilterSeries/FilterSeriesFilterExamples.png" style="width: 80%;" alt="TMS Software Delphi  Components tmsfncuipack"><br></p>

<h3>Filtering Without Filter Expressions</h3>

<p>The goal of the Filter View is simple.</p>

<p>Instead of asking users to create filter expressions, the application presents familiar controls that contribute to the filter automatically.</p>

<p>The user never needs to know that a filter expression exists.</p>

<p>Every interaction updates the underlying <b>TTMSFNCFilterBuilder</b>, keeping the filter synchronized in the background.</p>

<p>For example, instead of writing:</p>

<pre class="delphi" name="code">Country = &#39;Belgium&#39; AND Age &gt; 18</pre>

<p>The user simply:</p>

<ul>
<li>Selects "Belgium" from a ComboBox</li>
<li>Moves a slider to 18</li>
</ul>

<p>The Filter View generates exactly the same filter structure that we built manually in the previous blog posts.</p><p><br></p>

<h3>The Filter View Architecture</h3>

<p>Internally, every filter control contributes one or more expressions to the same FilterBuilder.</p>

<p>This means every control is responsible for a small part of the complete filter.</p>

<p>For example:</p>

<ul>
<li>a CheckBox can generate a Boolean expression</li>
<li>a ComboBox selects one value from a predefined list</li>
<li>a DatePicker filters on a date</li>
<li>a RangeSlider creates both a minimum and maximum value</li>
<li>a ValueEdit combines a comparison operator with user input</li>
</ul>

<p>The FilterBuilder combines all these expressions into one structured filter.</p>

<p>Because the same FilterBuilder is used, everything we discussed in the previous blogs still applies.</p>

<ul>
<li>Nested groups</li>
<li>Different output formats</li>
<li>Parsing</li>
<li>Validation</li>
</ul>

<p>The Filter View simply becomes another way of building that same filter.</p>

<p style="text-align: center; "><img src="https://tmssoftware.com/site/img/blog/fncui/filterview.png" style="width: 80%" alt="TMS Software Delphi  Components tmsfncuipack"><br></p>

<h3>Built-In Filter Controls</h3>

<p>The Filter View already contains a rich collection of controls that cover many common filtering scenarios.</p>

<ul>
<li>CheckBox</li>
<li>CheckGroup</li>
<li>RadioButton</li>
<li>RadioGroup</li>
<li>ComboBox</li>
<li>DatePicker</li>
<li>TrackBar</li>
<li>RangeSlider</li>
<li>ValueEdit</li>
</ul>

<p>Each control automatically updates its own FilterExpression whenever its value changes.</p>

<p>Because every control understands the type of data it represents, the resulting filter remains consistent.</p><p><br></p>

<h3>Configuring Expressions</h3>

<p>Every filter control has an associated FilterExpression.</p>

<p>This determines which part of the data the control affects.</p>

<p>Typically this consists of:</p>

<ul>
<li>the field name</li>
<li>the field type</li>
<li>the comparison operator</li>
<li>an optional default value</li>
</ul>

<p>For example, a ComboBox filtering countries can be configured like this:</p>

<pre class="delphi" name="code">FilterComboBox.FilterExpression.FilterFieldName := &#39;Country&#39;;
FilterComboBox.FilterExpression.FilterFieldType := fdtText;
FilterComboBox.FilterExpression.ExpressionOperator := feoEqual;</pre>

<p>Whenever the selected value changes, the filter expression is updated automatically.</p>

<p>Likewise, a CheckBox could represent:</p>

<pre class="delphi" name="code">Active = True</pre>

<p>without requiring any additional filter logic.</p><p><br></p>

<h3>Building Complex Filters Naturally</h3>

<p>Individual controls are useful, but real applications often require multiple filters working together.</p>

<p>Suppose we want users to filter:</p>

<ul>
<li>Employees from Belgium</li>
<li>Who are currently active</li>
<li>And joined after 2020</li>
</ul>

<p>Instead of writing:</p>

<pre class="delphi" name="code">Country = &#39;Belgium&#39;
AND Active = True
AND JoinDate &gt; EncodeDate(2020,1,1)</pre>

<p>The interface simply contains:</p>

<ul>
<li>a Country ComboBox</li>
<li>an Active CheckBox</li>
<li>a Join DatePicker</li>
</ul>

<p>Every control updates its own expression, while the FilterBuilder combines them into one filter automatically.</p><p><br></p>

<h3>Grouping Controls</h3>

<p>Just like FilterBuilder supports nested groups, the Filter View can visually group controls together.</p>

<p>A GroupPanel represents one filter group.</p>

<p>Groups can use either <b>AND</b> or <b>OR</b> operators.</p>

<p>This makes it easy to recreate even complex nested filter structures without exposing that complexity to the user.</p><p><br></p>

<p align="center">
<img src="https://tmssoftware.com/site/img/blog/fncui/filterviewdatagrid.png" style="width: 80%;" alt="TMS Software Delphi Components tmsfncuipack"></p><p align="center"><br></p>

<h3>More Than Built-In Controls</h3>

<p>The built-in controls cover many common scenarios, but they are certainly not the limit.</p>

<p>TTMSFNCFilterView was designed to be extensible.</p>

<p>You can integrate virtually any existing control by using the <b>TTMSFNCFilterViewControlContainer</b>, or register your own controls directly with the Filter View.</p>

<p>This allows domain-specific interfaces without sacrificing the underlying filtering logic.</p>

<p>We&#39;ve covered custom controls in more detail <a href="https://tmssoftware.com/site/blog.asp?post=2417">in a separate blog post</a>, but the important takeaway is that the Filter View isn&#39;t limited to its built-in controls.</p><p><br></p>

<h3>One Filter, Multiple Outputs</h3>

<p>Because the Filter View builds upon the same FilterBuilder introduced earlier in this series, everything remains compatible.</p>

<p>You can still:</p>

<ul>
<li>Generate filter text.</li>
<li>Assign the result to a DataSet.</li>
<li>Validate rows.</li>
<li>Filter object collections.</li>
<li>Generate different filter formats.</li>
</ul>

<p>The only difference is how the filter is created.</p>

<p>Instead of code or dialogs, the user simply interacts with the application.</p><p><br></p>

<h3>Why Use TTMSFNCFilterView?</h3>

<p>The Filter View provides a different user experience compared to a traditional filter dialog.</p>

<ul>
<li>Users interact with familiar controls instead of filter expressions.</li>
<li>Filtering updates automatically while users work.</li>
<li>The FilterBuilder stays synchronized behind the scenes.</li>
<li>Complex filtering logic becomes easier to understand.</li>
<li>The same filtering logic can still be reused throughout the application.</li>
</ul>

<p>For many applications, this feels much more natural than asking users to open a separate filter dialog.</p><p><br></p>

<h3>Next Step</h3>

<p>So far in this series, we&#39;ve focused on creating filters.</p>

<p>But filtering can do much more than determining which records are visible.What if the same filtering logic could also change colors, hide controls, enable actions, or modify properties?In the next blog post, we&#39;ll introduce <b>TTMSFNCFilterRulesManager</b>, where filter expressions become the starting point for dynamic application behavior.</p>
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Introducing TMS.NET Maps closed beta: FNC Maps Comes to .NET]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2502</link>
		<author>Bradley Velghe</author>
		<pubDate>Thu, 16 Jul 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		<p>For years, <b>FNC Maps</b> has given VCL and FMX developers one component that talks to Google Maps, Azure Maps, HERE, Mapbox, Leaflet and half a dozen other providers through a single, consistent API. It is one of those components people forget is doing something hard, because it never feels hard to use.</p>

<p>We kept getting the same question from teams who had also moved part of their stack to .NET: <i>"can we get that in WinForms, WPF, MAUI or Blazor?"</i></p>

<p>Today we are revealing the answer: <b>TMS.NET Maps</b>, a full port of the FNC Maps engine to .NET, and it is now open for a closed beta.</p>

<ul>
<li><b>One control, four platforms</b> &#151; WinForms, WPF, .NET MAUI and Blazor all share the exact same async API, so the code you write for a desktop app largely drops straight into a mobile or web one.</li>
<li><b>Twelve mapping and location providers</b> &#151; Google, Azure Maps, HERE, Mapbox, Leaflet/OpenStreetMap, OpenLayers, Apple MapKit, TomTom, GeoApify, OpenRouteService and IPStack, selectable at runtime.</li>
<li><b>Batteries included</b> &#151; markers, shapes, clustering, heatmaps, drawing tools, popups, custom overlays and companion services for geocoding, directions, elevation, places and IP location all ship in the box.</li>
</ul>

<p><br></p>

<img src="https://www.tmssoftware.com/site/img/blog/dotnet_maps_hero.png" alt="TMS.NET Maps &#151; one map control, four .NET platforms" style="max-width:100%;height:auto;border-radius:12px;">

<p><br></p>

<h3>Why we built it</h3>

<p>FNC Maps exists because Delphi developers should not have to learn a different mapping SDK for every provider, and should not have to rewrite their map code when they move a project from VCL to FMX, or from desktop to mobile.</p>

<p>The same argument holds even more strongly in .NET, where a single company might have a WinForms line-of-business app, a WPF dashboard, a MAUI field app and a Blazor customer portal, all wanting the same "show a map, drop some pins, draw a route" functionality. Nobody wants to learn four different map SDKs, four different marker models and four different event systems to do the same thing four times.</p>

<p>So TMS.NET Maps is not a new component with a similar name &#151; it is the same design philosophy as FNC Maps, rebuilt natively for .NET: one shared core, one control per platform, and a WebView-based rendering layer underneath so every provider&#39;s own JavaScript map engine can be used as-is, at full fidelity.</p>

<p><br></p>

<img src="https://www.tmssoftware.com/site/img/blog/dotnet_maps_architecture.png" alt="TMS.NET Maps architecture: shared MapControl over a WebView bridge, fanning out to twelve map providers" style="max-width:100%;height:auto;border-radius:12px;">
<p><i>One shared core, one control per platform, every provider&#39;s own map engine underneath.</i></p>

<p><br></p>

<h3>What&#39;s inside</h3>

<p>Everything below is implemented once, in the shared core, and works identically across all four platforms.</p>

<ul>
<li><b>Overlays</b> &#151; markers (with custom icons, drag support, drop-in animation and info windows), polylines (including geodesic lines and directional arrow symbols), polygons, rectangles and circles, all with full add/update/remove/clear APIs.</li>
<li><b>Clustering</b> &#151; group large marker sets into clusters automatically as the user zooms, with configurable clustering options.</li>
<li><b>Heatmaps</b> &#151; render weighted point data as a heat layer, independent of markers.</li>
<li><b>Drawing tools</b> &#151; let end users draw shapes directly on the map and get notified when they finish.</li>
<li><b>Popups and custom overlays</b> &#151; anchored popups for ad-hoc content, plus HTML element containers you can dock to map corners or center for custom UI (legends, search boxes, mini dashboards).</li>
<li><b>A genuinely rich event model</b> &#151; click, right-click, double-click, hover, drag and move events are exposed consistently for the map itself and for every overlay type (markers, polylines, polygons, circles, rectangles).</li>
<li><b>Companion services</b> &#151; <code>Geocoding</code>, <code>Directions</code>, <code>Elevation</code>, <code>Places</code>, <code>StaticMap</code> and <code>Location</code> (IP-based geolocation) ship as separate packages you can use standalone, even in a backend service with no map UI at all. A dedicated <code>RouteCalculator</code> service builds on top of Directions for turn-by-turn, navigation-style scenarios. (More on these below.)</li>
<li><b>Runtime provider switching</b> &#151; call one method to swap the entire map from, say, Google to Azure Maps, and your markers, lines and shapes carry over automatically.</li>
</ul>

<p><br></p>

<img src="https://www.tmssoftware.com/site/img/blog/dotnet_maps_features.png" alt="TMS.NET Maps features: clustering, heatmaps, drawing tools, routing, popups and custom overlays" style="max-width:100%;height:auto;border-radius:12px;">

<p><br></p>

<h3>A map in ten lines</h3>

<p>Here is the entire "hello world" for the WPF control &#151; the WinForms, MAUI and Blazor versions of this are almost identical.</p>

<pre class="xml" name="code">&lt;Window x:Class="QuickStart.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:maps="clr-namespace:TMS.Maps.Wpf;assembly=TMS.Maps.UI.Wpf"
        Title="TMS.NET Maps Quick Start" Height="600" Width="900"&gt;

    &lt;maps:MapControl x:Name="Map"
                      Provider="Google"
                      ApiKey="YOUR_GOOGLE_MAPS_KEY"
                      CenterLatitude="40.7128" CenterLongitude="-74.0060"
                      ZoomLevel="10"
                      MapReady="Map_Ready"/&gt;
&lt;/Window&gt;</pre>

<pre class="csharp" name="code">private async void Map_Ready(object? sender, EventArgs e)
{
    await Map.AddMarkerAsync(new MapMarker(new Coordinate(40.7128, -74.0060), "New York")
    {
        Options = new MarkerOptions { Animation = MarkerAnimation.Drop }
    });

    await Map.FitToMarkersAsync();
}</pre>

<p>That is a fully interactive map, panned and zoomed to fit its markers, with zero JavaScript written by you.</p>

<p><br></p>

<h3>Bonus: swap providers without losing your overlays</h3>

<p>Because every provider implements the same shared core, switching the backing map engine at runtime is one call &#151; useful for A/B testing a provider, falling back when a key is missing, or just giving end users a choice.</p>

<pre class="csharp" name="code">// Markers, lines and shapes carry over to the new provider automatically.
await Map.SwitchProviderAsync(Providers.Azure, azureMapsApiKey);</pre>

<p><br></p>

<h3>Beyond the map: the service layer</h3>

<p>Not every scenario needs a rendered map. An ASP.NET Core API validating an address, a nightly batch job geocoding a CSV of customers, or a console tool generating a static map thumbnail for an email &#151; none of that needs a <code>MapControl</code>, a WebView, or a reference to any UI framework at all.</p>

<p>Six of the seven companion packages are plain service classes: construct one with a provider and an API key, call an async method, get a typed result back.</p>

<pre class="csharp" name="code">using var geocoding = new GeocodingService(Providers.Google, apiKey);

var result = await geocoding.GetAsync("1600 Amphitheatre Parkway, Mountain View, CA");
if (result.Success)
{
    var match = result.Data.First();
    Console.WriteLine($"{match.FormattedAddress} -&gt; {match.Coordinate.Latitude}, {match.Coordinate.Longitude}");
}</pre>

<p>Directions follows the same shape, and returns distance, duration and the route geometry:</p>

<pre class="csharp" name="code">var directions = new DirectionsService(Providers.Google, apiKey);

var routes = await directions.GetAsync(
    new Coordinate(52.3676, 4.9041),   // Amsterdam
    new Coordinate(50.8503, 4.3517));  // Brussels

var route = routes.Data.First();
Console.WriteLine($"{route.Summary}: {route.Distance / 1000:F1} km, {route.Duration / 60} min");</pre>

<p>The full lineup:</p>

<ul>
<li><b>Geocoding</b> &#151; address to coordinates and back, via <code>GetAsync(string address)</code> or <code>GetAsync(Coordinate)</code>. Backed by Google, Azure, HERE, Mapbox, OpenRouteService and GeoApify.</li>
<li><b>Directions</b> &#151; routes between two points or a full waypoint list, returning distance, duration, turn-by-turn steps and the route geometry. Backed by Google, Azure, HERE, Mapbox, OpenRouteService, GeoApify and TomTom.</li>
<li><b>Elevation</b> &#151; elevation for a single coordinate or a whole list at once, for terrain profiles. Backed by Google and OpenRouteService.</li>
<li><b>Places</b> &#151; search, autocomplete and place details through one <code>GetAsync(PlacesRequest)</code> call. Backed by Google, Azure, HERE, Apple MapKit, GeoApify and TomTom.</li>
<li><b>StaticMap</b> &#151; either a ready-to-use image URL (<code>GetUrl</code>, synchronous, no HTTP call) or the actual image bytes as a <code>Stream</code> (<code>GetAsync</code>) &#151; for emails, PDFs or thumbnails with no map control anywhere in sight. Backed by Google, Azure, HERE, Mapbox and TomTom.</li>
<li><b>Location</b> &#151; IP-based geolocation, no GPS and no user permission prompt required. Backed by Google and IPStack.</li>
</ul>

<p>Every one of these takes only a <code>Providers</code> value and an API key to construct &#151; no map control, no WebView, no reference to WinForms, WPF, MAUI or Blazor at all. They are just as at home in an ASP.NET Core service or a background job as in a UI project.</p>

<h4>RouteCalculator: more than a thin wrapper</h4>

<p><code>RouteCalculator</code> sits a layer above Directions. Instead of a one-shot "get me a route," it models a stateful, editable route plan: build it, add waypoints, undo and redo changes, save and reload it, and optionally have it render and update itself on a live map.</p>

<pre class="csharp" name="code">var directionsService = new DirectionsService(Providers.Google, apiKey);
var routeCalculator = new RouteCalculatorService(directionsService);

var plan = await routeCalculator.CalculateRouteAsync(
    "Amsterdam, Netherlands",
    "Brussels, Belgium");

foreach (var segment in plan.Data!.Segments)
    Console.WriteLine($"{segment.Distance / 1000:F1} km, {segment.Duration / 60} min");</pre>

<p>Pass it an <code>IRouteRenderer</code> &#151; the built-in <code>MapRouteRenderer</code> does this for a <code>MapControl</code> &#151; and the same plan draws and updates itself on screen as it changes. Nothing about the service itself requires that you do so, which is what makes it equally useful in a delivery-planning backend with no map in sight.</p>

<p><br></p>

<h3>Why this matters</h3>

<ul>
<li>One API to learn, not four &#151; the same marker, shape and event code works on desktop, mobile and web.</li>
<li>No provider lock-in &#151; pick Google today, add Azure or HERE tomorrow, or offer a free no-key option with Leaflet or OpenLayers, without rewriting your map code.</li>
<li>A WebView-based renderer means every provider&#39;s own map engine runs at native fidelity, not a reimplementation of it.</li>
<li>Geocoding, directions, elevation, places and location services are available standalone, so backend-only projects can use them without any UI dependency at all.</li>
<li>It carries forward everything FNC Maps developers already trust, now available to .NET teams.</li>
</ul>

<p><br></p>

<img src="https://www.tmssoftware.com/site/img/blog/dotnet_maps_beta_banner.png" alt="Join the TMS.NET Maps closed beta" style="max-width:100%;height:auto;border-radius:12px;">

<p><br></p>

<h3>Join the closed beta</h3>

<p>TMS.NET Maps targets .NET 10 and is available today as a private, evaluation-only closed beta covering WinForms, WPF, MAUI and Blazor.</p>

<p>Sign up on the <a href="https://www.tmssoftware.com/site/dotnet">TMS .NET page</a> and we&#39;ll get you access to the private package feed, the getting-started guide, and a direct channel to the team building it. Your feedback is exactly what will shape the 1.0 release.</p>

<p><br></p>

<hr>

<p><b><a href="https://www.tmssoftware.com/site/dotnet">TMS.NET Maps</a></b> brings the same cross-platform mapping engine behind TMS FNC Maps to WinForms, WPF, .NET MAUI and Blazor &#151; one API, twelve providers, and every overlay, clustering, heatmap and routing feature you&#39;d expect, in native .NET. <a href="https://www.tmssoftware.com/site/dotnet">Sign up for the closed beta</a>.</p>
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Filtering in Delphi: See the Filter Dialog in Action]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2499</link>
		<author>Gjalt Vanhouwaert</author>
		<pubDate>Thu, 16 Jul 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		<p>In the <a href="https://tmssoftware.com/site/blog.asp?post=2497">previous blog post</a>, we explored how <b>TTMSFNCDataSetFilterDialog</b> and <b>TTMSFNCFilterDialog</b> allow users to build complex filters through a guided visual interface instead of manually writing filter expressions.</p>

<p>While screenshots explain the concepts, seeing the components in action often gives a much better idea of how quickly complex filters can be created.</p>

<p>That&#39;s why we&#39;d like to share the following demonstration, showcasing both dialogs in a real application.</p>

<p style="text-align:center;">
<iframe frameborder="0" src="//www.youtube.com/embed/IdNjcSSXUu0" width="640" height="360" class="note-video-clip"></iframe>
</p>

<h3>What You&#39;ll See</h3>

<p>The video starts with a quick demonstration of the <b>TTMSFNCDataSetFilterDialog</b>, showing how a filter can be created for a <code>TDataSet</code> without manually writing the filter expression.</p>

<p>It then continues with a more extensive demonstration using <b>TTMSFNCFilterDialog</b> together with <b>TTMSFNCDataGrid</b>, where you&#39;ll see how users can create even complex nested filters through a visual interface.</p>

<p>Among other things, the video demonstrates:</p>

<ul>
<li>Selecting fields instead of typing field names.</li>
<li>Showing only operators that are valid for the selected field type.</li>
<li>Building nested <code>AND</code> and <code>OR</code> groups visually.</li>
<li>Generating the correct filter expression automatically.</li>
<li>Applying the resulting filter directly to the grid.<br><br></li>
</ul>

<h3>New Since TMS FNC UI Pack 7.2</h3>

<p>The video was recorded before the release of <b>TMS FNC UI Pack 7.2</b>.</p>

<p>Since then, another useful feature has been added.</p>

<p>The <b>TTMSFNCDataSetFilterDialog</b> is now also available as the design-time editor for the <code>Filter</code> property of a <code>TDataSet</code>.</p>

<p>Instead of manually typing a filter expression in the Object Inspector, you can now simply open the visual editor, build the filter, and have the generated filter text written back to the dataset automatically.</p>

<p style="text-align:center;"><br></p>

<p>This makes creating dataset filters during development considerably easier, especially for more complex expressions.</p><p><br></p>

<h3>The Same FilterBuilder Foundation</h3>

<p>Both dialogs are built on top of the same <b>TTMSFNCFilterBuilder</b> introduced earlier in this series.</p>

<p>Whether the filter is created programmatically, through the visual dialogs, or later through the Filter View, the same structured filtering model is used throughout.</p>

<p>This means the generated filter can still be:</p>

<ul>
<li>assigned directly to a <code>TDataSet</code></li>
<li>parsed back into a visual representation</li>
<li>validated against your own data</li>
<li>generated in different filter formats<br><br></li>
</ul>

<h3>What&#39;s Next?</h3>

<p>So far we&#39;ve looked at building filters either through code or through a dialog.</p>

<p>In the next part of this series, we&#39;ll remove the dialog altogether.</p>

<p>Instead, users will simply interact with checkboxes, combo boxes, sliders and date pickers while the filter is updated automatically in the background using <b>TTMSFNCFilterView</b>.</p>
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Let your AI work seamlessly with your database data via a Delphi MCP server]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2498</link>
		<author>Bradley Velghe</author>
		<pubDate>Wed, 15 Jul 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		<p><b>Model Context Protocol (MCP)</b> is the standard that lets AI clients like Claude Desktop, OpenAI Desktop, Cursor, ... call out to your own tools. An MCP server exposes a set of functions, the assistant reads their descriptions, decides when to call them, and feeds the results back into the conversation.</p>

<p>It is the cleanest way yet to let a language model touch a real system. And a database is the obvious first system to reach for.</p>

<p>But a database is also the scariest one. You do not want an over-eager model running <code>DROP TABLE</code> on production data.</p>

<p>So in this post we build an MCP server in Delphi that is generic, configurable and safe:</p>

<ul> <li>generic, because it works with any engine FireDAC can reach</li> <li>configurable, because you decide what the AI is allowed to do</li> <li>safe by construction, because in read-only mode there is literally no tool that can write</li> </ul>

<p>Everything below is a real, compiling demo from the TMS MCP SDK. The JSON in the result blocks is copied straight from a live run against SQL Server.</p><p><br></p>

<h3>The shape of it</h3>

<p>One executable sits between the MCP client and FireDAC.</p>

<p>It owns the database connection for its whole lifetime, enforces an access mode and a SQL-safety check on the way in, and turns result sets into JSON on the way out.</p>

<p>Because it is driven by a connection string and by FireDAC&#39;s engine-agnostic metadata API, the same binary speaks to SQLite, PostgreSQL, MySQL, SQL Server, Oracle, Firebird and ODBC.</p>

<p><svg viewBox="0 0 860 300" role="img" aria-label="One binary between the MCP client and FireDAC; the connection string decides the engine." style="max-width:100%;height:auto;">
  <defs>
    <marker id="fdmcpArrow1" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#0E7C86"></path></marker>
  </defs>
  <rect x="20" y="112" width="120" height="72" rx="12" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect>
  <text x="80" y="142" text-anchor="middle" font-family="Consolas,monospace" font-size="12" fill="#14181E">Claude</text>
  <text x="80" y="160" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#6E7681">MCP client</text>
  <path d="M140,148 H206" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow1)"></path>
  <text x="173" y="140" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#6E7681">JSON-RPC</text>
  <text x="173" y="176" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#6E7681">stdio</text>
  <rect x="214" y="86" width="220" height="124" rx="12" fill="#0F141B"></rect>
  <text x="324" y="116" text-anchor="middle" font-family="Consolas,monospace" font-size="12" fill="#D7DEE6">FireDAC MCP Server</text>
  <rect x="234" y="130" width="180" height="26" rx="6" fill="#161D27" stroke="#27313D"></rect>
  <text x="324" y="147" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#9FB0C0">access mode gate</text>
  <rect x="234" y="164" width="180" height="26" rx="6" fill="#161D27" stroke="#27313D"></rect>
  <text x="324" y="181" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#9FB0C0">SQL classifier</text>
  <path d="M434,148 H500" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow1)"></path>
  <text x="467" y="140" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#6E7681">TFDQuery</text>
  <rect x="508" y="112" width="104" height="72" rx="12" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect>
  <text x="560" y="142" text-anchor="middle" font-family="Consolas,monospace" font-size="12" fill="#14181E">FireDAC</text>
  <text x="560" y="160" text-anchor="middle" font-family="Consolas,monospace" font-size="10" fill="#6E7681">drivers</text>
  <path d="M612,148 H668" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow1)"></path>
  <rect x="676" y="30" width="164" height="30" rx="8" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect><text x="758" y="50" text-anchor="middle" font-family="Consolas,monospace" font-size="10.5" fill="#0A5A62">SQLite</text>
  <rect x="676" y="66" width="164" height="30" rx="8" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect><text x="758" y="86" text-anchor="middle" font-family="Consolas,monospace" font-size="10.5" fill="#0A5A62">PostgreSQL</text>
  <rect x="676" y="102" width="164" height="30" rx="8" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect><text x="758" y="122" text-anchor="middle" font-family="Consolas,monospace" font-size="10.5" fill="#0A5A62">MySQL / MariaDB</text>
  <rect x="676" y="138" width="164" height="30" rx="8" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect><text x="758" y="158" text-anchor="middle" font-family="Consolas,monospace" font-size="10.5" fill="#0A5A62">SQL Server</text>
  <rect x="676" y="174" width="164" height="30" rx="8" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect><text x="758" y="194" text-anchor="middle" font-family="Consolas,monospace" font-size="10.5" fill="#0A5A62">Oracle</text>
  <rect x="676" y="210" width="164" height="30" rx="8" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect><text x="758" y="230" text-anchor="middle" font-family="Consolas,monospace" font-size="10.5" fill="#0A5A62">Firebird / ODBC</text>
  <path d="M668,148 C674,120 670,60 676,45" stroke="#0E7C86" stroke-width="2" fill="none" opacity="0.5"></path>
  <path d="M668,148 H676" stroke="#0E7C86" stroke-width="2" fill="none" opacity="0.5"></path>
  <path d="M668,148 C674,176 670,214 676,225" stroke="#0E7C86" stroke-width="2" fill="none" opacity="0.5"></path>
</svg></p>
<p><i>One binary; the connection string decides the engine.</i></p><p><br></p>

<h3>A server in thirty lines</h3>

<p>The entry point is almost boring, which is the point.</p>

<p>You link the FireDAC driver units for the engines you want to support, create a <b>TTMSMCPServer</b>, register your tools, and run. <code>Start</code> wires up the stdio transport automatically, and <code>Run</code> is the blocking JSON-RPC loop.</p>

<pre class="delphi" name="code">uses
  // Link every engine you want this build to speak to:
  FireDAC.Phys.SQLite, FireDAC.Phys.PG,   FireDAC.Phys.MySQL,
  FireDAC.Phys.MSSQL,  FireDAC.Phys.ODBC, FireDAC.Phys.Oracle,
  FireDAC.ConsoleUI.Wait, // headless: no VCL/FMX forms
  TMS.MCP.Server, TMS.MCP.Transport.STDIO,
  DBConnectionManager;

var
  Server : TTMSMCPServer;
begin
  Manager := TDBConnectionManager.Create(Config);   // owns the connection
  Manager.Connect;

  Server := TTMSMCPServer.Create(nil);
  Server.ServerName := &#39;FireDACMCPServer&#39;;
  Manager.RegisterTools(Server);   // db_info, list_tables, run_query, ...

  Server.Start;   // stdio transport
  Server.Run;     // blocking read/dispatch/write loop
end.</pre>

<p>Everything interesting lives in <code>TDBConnectionManager</code>. Let&#39;s build it up piece by piece.</p><p><br></p>

<h3>One connection string, any engine</h3>

<p>FireDAC&#39;s <code>TFDConnection</code> can be configured entirely from a connection string, such as <code>DriverID=...;Server=...;Database=...</code>.</p>

<p>That is the whole trick. Whatever the user passes, FireDAC parses it and dispatches to the right driver.</p>

<p>We add auto-reconnect for resilience and, in read-only mode, flip the datasets read-only as a second line of defence.</p>

<pre class="delphi" name="code">FConnection := TFDConnection.Create(nil);
FConnection.LoginPrompt := False;

// The one line that makes this engine-agnostic:
FConnection.ConnectionString := AConnStr;   // "DriverID=PG;Server=...;Database=..."

FConnection.ResourceOptions.AutoReconnect := True;
if FMode = amReadOnly then
  FConnection.UpdateOptions.ReadOnly := True;   // belt and braces

FConnection.Connected := True;</pre><p><br></p>

<h3>Safety, in three modes</h3>

<p>The single most important design decision is that capability follows mode.</p>

<p>A read-only server does not merely refuse writes. It never registers a tool that could write, so the assistant never sees one.</p>

<ul> <li><b>readonly</b> (default): SELECT and metadata only. It exposes <code>db_info</code>, <code>list_tables</code>, <code>describe_table</code> and <code>run_query</code>. No mutation tool exists.</li> <li><b>readwrite</b>: adds <code>run_statement</code> for INSERT, UPDATE, DELETE and MERGE. Schema changes are still refused.</li> <li><b>full</b>: also allows DDL (CREATE, ALTER, DROP, TRUNCATE) and the otherwise-blocked statements. For sandboxes and admin work.</li> </ul>

<p>That is enforced by a single conditional at registration time:</p>

<pre class="delphi" name="code">// Read tools are always registered.
FServer.Tools.RegisterTool(&#39;run_query&#39;, &#39;Execute a read-only query&#39;, ToolRunQuery);

// The write tool only exists when the mode permits it:
if FConfig.AccessMode &lt;&gt; amReadOnly then
  FServer.Tools.RegisterTool(&#39;run_statement&#39;, &#39;...&#39;, ToolRunStatement);</pre>

<p>Belt and braces, though. Even a registered tool runs its SQL past a classifier first.</p>

<p>We blank out comments and string literals so the leading keyword cannot be smuggled past us, then map it to a category and check it against the mode. Multi-statement stacking, such as <code>SELECT 1; DROP TABLE users</code>, is rejected unless you explicitly opt in.</p>

<pre class="delphi" name="code">function Classify(const ASQL: string): TSQLCategory;
var KW: string;
begin
  // StripSQL blanks comments and quoted text before we read the keyword
  KW := LeadingKeyword(StripSQL(ASQL));
  if      (KW=&#39;SELECT&#39;) or (KW=&#39;WITH&#39;) or (KW=&#39;EXPLAIN&#39;) then Result := scRead
  else if (KW=&#39;INSERT&#39;) or (KW=&#39;UPDATE&#39;) or (KW=&#39;DELETE&#39;) then Result := scWrite
  else if (KW=&#39;CREATE&#39;) or (KW=&#39;ALTER&#39;)  or (KW=&#39;DROP&#39;)   then Result := scDDL
  else Result := scDangerous;   // ATTACH, PRAGMA, EXEC, GRANT, ...
end;</pre>

<p><svg viewBox="0 0 860 132" role="img" aria-label="Every call is classified and mode-checked before FireDAC sees it." style="max-width:100%;height:auto;">
  <defs>
    <marker id="fdmcpArrow2" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#0E7C86"></path></marker>
  </defs>
  <rect x="14" y="46" width="128" height="42" rx="9" fill="#0F141B"></rect>
  <text x="78" y="64" text-anchor="middle" font-family="Consolas,monospace" font-size="11.5" fill="#D7DEE6">tool call</text>
  <text x="78" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">sql + params</text>
  <path d="M142,67 H176" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow2)"></path>
  <rect x="178" y="46" width="140" height="42" rx="9" fill="#0F141B"></rect>
  <text x="248" y="64" text-anchor="middle" font-family="Consolas,monospace" font-size="11.5" fill="#D7DEE6">classify</text>
  <text x="248" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">read / write / ddl</text>
  <path d="M318,67 H352" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow2)"></path>
  <rect x="354" y="46" width="140" height="42" rx="9" fill="#0F141B"></rect>
  <text x="424" y="64" text-anchor="middle" font-family="Consolas,monospace" font-size="11.5" fill="#D7DEE6">mode check</text>
  <text x="424" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">allow / reject</text>
  <path d="M494,67 H528" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow2)"></path>
  <rect x="530" y="46" width="150" height="42" rx="9" fill="#0F141B"></rect>
  <text x="605" y="64" text-anchor="middle" font-family="Consolas,monospace" font-size="11.5" fill="#D7DEE6">FireDAC exec</text>
  <text x="605" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">bind &amp; run</text>
  <path d="M680,67 H714" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#fdmcpArrow2)"></path>
  <rect x="716" y="46" width="130" height="42" rx="9" fill="#0E7C86"></rect>
  <text x="781" y="64" text-anchor="middle" font-family="Consolas,monospace" font-size="11.5" fill="#ffffff">JSON</text>
  <text x="781" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#CDEDEE">rows + cols</text>
</svg></p>
<p><i>Every call is classified and mode-checked before FireDAC sees it.</i></p><p><br></p>

<h3>Turning a row into JSON</h3>

<p>The other half of the job is marshalling.</p>

<p>A <code>TField</code> becomes the right JSON type. Numbers stay numbers, booleans stay booleans, dates go out as ISO-8601, and binary columns are base64-encoded with a size cap so a large blob cannot flood the transport.</p>

<pre class="delphi" name="code">case AField.DataType of
  ftBoolean:
    Result := TJSONBool.Create(AField.AsBoolean);
  ftInteger, ftLargeint, ftAutoInc:
    Result := TJSONNumber.Create(AField.AsLargeInt);
  ftFloat, ftCurrency, ftBCD, ftFMTBcd:
    Result := TJSONNumber.Create(AField.AsFloat);
  ftDate, ftDateTime, ftTimeStamp:
    Result := TJSONString.Create(FormatDateTime(&#39;yyyy-mm-dd"T"hh:nn:ss&#39;, AField.AsDateTime));
  ftBlob, ftVarBytes, ftBytes:
    Result := TJSONString.Create(TNetEncoding.Base64.EncodeBytesToString(AField.AsBytes));
else
  Result := TJSONString.Create(AField.AsString);   // strings, GUIDs, memos
end;</pre>

<p>Point it at a SQL Server table with one column of every awkward type, and it round-trips cleanly:</p>

<ul> <li><code>DECIMAL(12,2)</code> becomes <code>1234.56</code></li> <li><code>MONEY</code> becomes <code>9999.99</code></li> <li><code>BIGINT</code> becomes <code>9000000000</code></li> <li><code>BIT</code> becomes <code>true</code></li> <li><code>DATETIME2</code> becomes <code>"2026-07-03T14:25:36"</code></li> <li><code>UNIQUEIDENTIFIER</code> becomes <code>"{0CFED1EF-...}"</code></li> <li><code>VARBINARY</code> (small) becomes a base64 string</li> <li><code>VARBINARY</code> (300 KB) becomes <code>"&lt;binary 300000 bytes...&gt;"</code></li> <li><code>NVARCHAR(MAX)</code> becomes <code>"caf&#233;, na&#239;ve, 日本語"</code></li> </ul><p><br></p>

<h3>Wiring it into Claude Desktop</h3>

<p>While our MCP server will work fine with any AI client, we present here the steps to make the integration with Claude Desktop.<br>Build the project (it is a plain Win32 console app), then add an entry to Claude Desktop&#39;s <code>claude_desktop_config.json</code>. The connection string and mode live in <code>args</code>:</p>

<pre class="js" name="code">{
  "mcpServers": {
    "SQLSandbox": {
      "command": "C:/.../FireDACMCPServer.exe",
      "args": [
        "-conn=DriverID=MSSQL;Server=localhost;Database=MCPSandbox;OSAuthent=Yes;ODBCAdvanced=TrustServerCertificate=yes",
        "-mode=readonly"
      ]
    }
  }
}</pre>

<p><b>A note on SQL Server and ODBC Driver 18.</b> Driver 18 encrypts by default and validates the server certificate, so against a local instance you will hit "the certificate chain was issued by an authority that is not trusted". Pass it through with <code>ODBCAdvanced=TrustServerCertificate=yes</code>. FireDAC&#39;s own <code>TrustServerCertificate</code> parameter is not forwarded to ODBC, only <code>ODBCAdvanced</code> works.</p><p><br></p>

<h3>What the assistant sees</h3>

<p>On connect, the client asks for the tool list. Notice the annotations. Read tools carry <code>readOnlyHint</code>, the write tool carries <code>destructiveHint</code>. Those hints help the model reason about what is safe to call unprompted.</p>

<pre class="js" name="code">{ "tools": [
  { "name": "db_info",        "annotations": { "readOnlyHint": true, "idempotentHint": true } },
  { "name": "list_tables",    "annotations": { "readOnlyHint": true } },
  { "name": "describe_table", "annotations": { "readOnlyHint": true } },
  { "name": "run_query",      "annotations": { "readOnlyHint": true } }
] }
// run_statement is absent - this is a read-only server.</pre>

<p>Now a real conversation. You ask a question in plain English:</p>

<p><i>"Which customers are in Springfield, and what have they ordered?"</i></p>

<p>Claude picks the <code>run_query</code> tool and writes the SQL itself:</p>

<pre class="sql" name="code">SELECT c.Name, o.Id, o.Status
FROM Customers c
JOIN Orders o ON o.CustomerId = c.Id
WHERE c.City = &#39;Springfield&#39;;</pre>

<p>Under the hood, that call returns a tidy envelope: columns, rows, a count, and a <code>truncated</code> flag so the model knows if it hit the row cap.</p>

<pre class="js" name="code">{
  "columns": [ { "name": "Id",   "type": "ftInteger" },
               { "name": "Name", "type": "ftWideString" },
               { "name": "City", "type": "ftWideString" } ],
  "rows": [ { "Id": 1, "Name": "Acme Corp", "City": "Springfield" } ],
  "rowCount": 1,
  "truncated": false
}</pre>

<p>The model reads that back and answers in words, for example that Acme Corp in Springfield has two orders, one shipped and one still pending.</p>

<p>And if the model gets ambitious on a read-only server, the guards answer for it, clearly enough that it can correct itself:</p>

<pre class="plain" name="code">// run_query "DROP TABLE person"
"run_query only accepts read-only statements. Use run_statement instead."

// run_query "SELECT 1; DROP TABLE person"
"Multiple SQL statements in a single call are not allowed."

// tools/call run_statement
"Unknown tool: run_statement"</pre><p><br></p>

<h3>Bonus: ask for the connection at runtime</h3>

<p>What if you do not want to bake the connection string into the config at all?</p>

<p>Start the server with no <code>-conn</code> and it comes up unconnected, exposing a single <code>connect</code> tool. Once the AI (or you, through it) supplies a connection string and a mode, the server connects and the database tools appear through a <code>tools/list_changed</code> notification.</p>

<pre class="js" name="code">// connect { driver_id: "MSSQL", server: "localhost",
//           database: "MCPSandbox", mode: "readwrite" }
{
  "connected": true,
  "driverId": "MSSQL",
  "accessMode": "readwrite",
  "message": "Connected. The database tools are now available."
}</pre>

<p>On a bidirectional transport you would use MCP elicitation to pop a form for the credentials. Over stdio, a <code>connect</code> tool is the clean, universal equivalent.</p><p><br></p>

<h3>Why this matters</h3>

<p>Wrapping a database as an MCP server this way gives a few practical advantages:</p>

<ul> <li>The assistant explores the schema itself through <code>list_tables</code> and <code>describe_table</code>, so you do not hard-code anything.</li> <li>The access mode is a single switch that decides read-only, read-write or full.</li> <li>In read-only mode there is no mutation tool at all, not just a blocked one.</li> <li>The SQL classifier stops statement stacking and out-of-mode statements before they reach the database.</li> <li>Every value comes back correctly typed, including dates, decimals, GUIDs and binary data.</li> <li>The very same binary becomes a Postgres, Oracle or SQLite agent just by changing the connection string.</li> </ul>

<p>The important part is that the safety is structural. The mode decides which tools exist, and the classifier is the backstop behind them.</p><p><br></p>

<h3>Get the demo</h3>

<p>The full source, <code>FireDACMCPServer.dpr</code> and <code>DBConnectionManager.pas</code>, is available on GitHub: <a href="https://github.com/tmssoftware/Database-MCP-Server">github.com/tmssoftware/Database-MCP-Server</a>.</p>

<p>A prebuilt Windows executable is included in the repository, so you can register it with Claude Desktop and try it against your own database right away, without compiling anything.</p>

<p>Clone it, point it at your own database, and start in <code>readonly</code>. You can always turn the dial up later.</p><p><br></p>

<hr>

<p><b><a href="https://www.tmssoftware.com/site/tmsaistudio.asp">TMS AI Studio</a></b> is the complete toolkit for bringing AI and the Model Context Protocol to your Delphi applications. Build MCP servers and clients, connect Claude and other assistants to your own tools and data, and add cloud AI to your VCL and FMX apps &#151; all in native Delphi. <a href="https://www.tmssoftware.com/site/tmsaistudio.asp">Discover TMS AI Studio</a>.</p>

		
		
		<br><br></P>]]></description>
	</item>
	</channel></rss>
