<?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>Thu, 3 Sep 2026 16:09: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[﻿Translate Your Delphi Application with Ease - Introducing TMS FNC Localization BETA]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2513</link>
		<author>Tunde Keller</author>
		<pubDate>Thu, 3 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		<div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog.png" style="width: 33%; float: right;" class="note-float-right" alt="TMS Software Delphi  Components tmsfnclocalization">When starting a new application, supporting multiple languages can easily end up somewhere near the bottom of the priority list. And when localization is added later to an existing or even long-running application, suddenly there are captions, messages, dialogs and other texts scattered all over the project that all need to be found, translated and kept in sync.</div><div><br></div><div>How do you collect all those texts? How do you keep translations organized? And how do you apply them to your application? This is exactly the kind of problem we like to solve with easy-to-use components!</div><div><br></div><div>Today, we are very excited to introduce a brand-new product entering BETA: <b>TMS FNC Localization</b>.</div><div><br></div><h4>From a Real-World Need to TMS FNC Localization</h4><div><div>For readers who have been following our blog, the name <b>Erwin Denissen</b> might already sound familiar.</div><div><br></div><div>Erwin is the developer behind <b>FontCreator </b>from High-Logic, which we featured earlier in <a href="https://www.tmssoftware.com/site/blog.asp?post=1292" target="_blank">one of our customer showcase</a> blog posts. As part of his own software development, Erwin also ran into the challenges that come with adding and maintaining localization in an application.</div><div><br></div><div>Not finding a solution that matched the workflow he was looking for, he started building his own.</div><div><br></div><div>What began as a solution for his own needs eventually brought Erwin and TMS Software together. We saw a lot of potential in his work, and from that cooperation the project grew further into the product we are presenting today as <b>TMS FNC Localization</b>.</div><div><br></div><div>We want to explicitly thank Erwin for the work, ideas and experience he has contributed to making this possible!</div><div><br></div><div><h4>Collect, Translate, Apply!</h4><div>At its core, TMS FNC Localization is built around a straightforward workflow: <b>collect </b>the texts, <b>translate </b>them and <b>apply </b>the selected language. Instead of having to build the complete localization infrastructure yourself, you get dedicated components for each step!</div><div><br></div><div><b>Collect</b></div><div><b><br></b></div><div>The first challenge is finding everything that needs to be translated, especially in an existing application. The <b>TTMSFNCLocalizationCollector </b>helps gather localizable content from your application instead of forcing you to manually hunt down every caption and text property.</div><div></div><pre class="delphi" name="code">//Analyze and collect the application - only applies to live forms
TMSFNCLocalizationCollector.AnalyzeApplication;
//Analyze and collect a given form
TMSFNCLocalizationCollector.AnalyzeForm(Self);</pre><div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog1.png" style="width: 100%;" alt="TMS Software Delphi  Components tmsfnclocalization"><br></div><div><br></div><div><b>Translate</b></div><div><b><br></b></div><div><div>Once the strings have been collected, they need to be managed and translated.</div><div><br></div><div>The <b>TTMSFNCLocalizationEditor</b> provides an easy way to work with the collected texts and the translations for the different languages. Opening the editor at runtime comes with the benefit of being able to see the translation changes immediately in your running application.</div><div><pre class="delphi" name="code">TMSFNCLocalizationEditor.Execute;</pre></div><div>Alternatively, open it at design time from the context menu of the TTMSFNCLocalizationCollector or any TForm. When the editor is triggered at design time, you can also see which forms haven&#39;t been discovered yet!</div><div><br></div><div>What if you don&#39;t want to manually translate every item? Use the <b>optional AI assistance</b> to generate translations and you&#39;ll only need to verify the results! We&#39;ve added support for a variety of AI services that you can use with your API key. These include <b>OpenAI, Claude, Gemini, Grok, Mistral and DeepSeek</b>. If you prefer local models, we have you covered via <b>Ollama and llama.cpp</b>.</div><div><br></div><div>Would you rather leave it to a professional? <b>XLIFF export and import </b>support is available, allowing you to outsource the work to professional translators using their own tools!</div><div><br></div><div>To help keep translations <b>reliable</b>, TMS FNC Localization comes with <b>built-in automatic text validation</b> for format specifiers, placeholders, accelerator keys, line breaks and whitespace, whether translations are entered manually, imported or generated with AI. It supports <b>plural-aware translations</b> as well, allowing the correct wording to be used depending on the quantity being displayed.&nbsp;This is especially useful for languages where pluralization rules go beyond a simple singular-versus-plural distinction.</div><div><br></div><div>On top of editing and verifying the translations, the TTMSFNCLocalizationEditor also lets you <b>manage the list of languages</b> you want to support, along with a <b>set of exclusion rules</b> for what should <b>not</b> be collected. Using the editor also means collection happens automatically, no need for additional code!</div><div><br></div><div>The editor <b>keeps the translation process separate</b> from the application itself. Adding another language or updating an existing translation becomes a matter of maintaining the localization data instead of changing forms or source code.</div><div><br></div><div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog2.png" style="width: 100%;" alt="TMS Software Delphi  Components tmsfnclocalization"><br></div><div><br></div><div><b>Apply</b></div><div><div><br></div><div>Finally, the <b>TTMSFNCLocalizationLocalizer </b>takes care of applying the selected language to the application! Point it to the folder where the localization files live, and calling the simple <b>TrySetLocale </b>method will immediately switch to the target language. This gives you fine control over when and how a language is set. If you&#39;d rather hand that choice to your users, TMS FNC Localization ships with a ready-made language picker, <b>TTMSFNCLocalizationCombobox</b>, that you can drop straight into your forms!</div><div><br></div><div>Together, these components turn localization into a <b>repeatable workflow</b> instead of a one-off solution. When another language needs to be added later, you can simply repeat the same step again!</div><div><br></div><div><img src="https://www.tmssoftware.com/site/img/blog/tmsfnclocalizationblog3.png" style="width: 100%;" alt="TMS Software Delphi  Components tmsfnclocalization"><br></div><div><br></div><h4>New or Existing, Localization Fits</h4><div><div>Of course, the easiest moment to think about localization is before writing the first line of an application. From the start, you can keep your strings in one place with <b>TTMSFNCLocalizationStringCatalog</b>. That way you can keep everything organized, central and easy to collect.</div><div><br></div><div>That said, one of the goals behind TMS FNC Localization is to make localization approachable <b>not only for new projects, but also for applications that already exist</b>.&nbsp;Localization no longer has to be an all-or-nothing decision made on day one. You can introduce it gradually and <b>grow it as the application grows</b>.</div><div><br></div><h4>Get Started with the BETA</h4><div><b><a href="https://www.tmssoftware.com/site/tmsallaccess.asp" target="_blank">TMS ALL-ACCESS</a></b> users can start experimenting with TMS FNC Localization <b>today</b>! The BETA is available through <a href="https://doc.tmssoftware.com/smartsetup/" target="_blank">TMS Smart Setup</a>, making it easy to add the product to your development environment and keep up with new BETA versions as they become available.</div><div><br></div><div>To help you get started, we have included demos with the installation and prepared documentation covering both the component API and the overall workflow.&nbsp;<a href="https://doc.tmssoftware.com/fnc/localization/" target="_blank">Click here</a> to open it on our new TMS Documentation site, the <a href="https://tmssoftware.com/site/blog.asp?post=2501" target="_blank">new home for all TMS product documentation</a>.</div><div><br></div><h4>Help Us Shape TMS FNC Localization</h4><div><div>Localization requirements can differ significantly between applications, and feedback from developers using the product on real projects is <b>extremely valuable</b>.</div><div><br></div><div>If you are an ALL-ACCESS user, we encourage you to <b>put it to work</b>! Try it on a new application or, even better, try it on that older project that someone has just decided needs to conquer three new markets. Let us know what works well, what could work better and what is missing from your localization workflow!</div><div><br></div><div>Feedback and findings can be shared with us through the dedicated <a href="https://support.tmssoftware.com/c/fnc/tms-fnc-localization/131" target="_blank">TMS Support Center category</a>. We are looking forward to seeing what you build with it and in how many languages!</div></div></div></div></div></div></div>
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Make Your Data Speak: Conditional Formatting in TMS FNC Data Grid 2.0]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2514</link>
		<author>Pieter Scheldeman</author>
		<pubDate>Wed, 2 Sep 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		<p><span style="background-color: darkgray; color: white; padding: 4px 8px; text-align: center; border-radius: 5px; font-size: 1.5em;">TMS FNC UI Pack 7.3</span></p><p><span style="background-color: darkorange; color: white; padding: 4px 8px; text-align: center; border-radius: 5px; font-size: 1.5em;">TMS FNC Data Grid 2.0</span></p>

<h4 style="color: rgb(0, 0, 0);">Intro</h4>
<p>A grid can contain every answer and still make the important ones hard to find. Which sales representatives are leading the quarter? Which targets are at risk? Where is growth slowing down? Traditionally, answering those questions visually meant writing custom drawing events and maintaining the same business logic in several places.</p>

<p>With <strong>TMS FNC UI Pack 7.3</strong>, we are releasing <strong>TMS FNC Data Grid 2.0</strong> and introducing a new declarative conditional-formatting engine for <code>TTMSFNCDataGrid</code>.</p>

<p>Conditional formatting lets the data decide how a cell or row should look. Add rules for values, text, dates, rankings, averages, duplicates or complete expressions, and the grid automatically applies fills, font styles, color scales, data bars and icon sets while it renders.</p>

<p><img src="https://www.tmssoftware.com/site/img/tmsfncuipack_7_3/conditional-formatting-grid.png" style="max-width: 100%;" data-filename="conditional-formatting-grid.png" alt="TMS FNC Data Grid 2.0 sales dashboard with color scales, data bars, icon sets and status highlights"></p>

<h4>From Raw Values to Instant Insight</h4>
<p>The new <code>ConditionalFormatting</code> property owns a collection of rules. Each rule defines three things:</p>

<ul>
<li><p>where it applies;</p></li>
<li><p>when it matches;</p></li>
<li><p>how the matching cell or row should be presented.</p></li>
</ul>

<p>Rules are evaluated automatically as the grid paints, and remain effective when users scroll, sort or filter. There is no need to repeat the same decisions in <code>OnGetCellLayout</code> or a custom drawing event.</p>

<p>The engine includes ten rule families:</p>

<ul>
<li><p>cell-value comparisons;</p></li>
<li><p>text matching;</p></li>
<li><p>date conditions;</p></li>
<li><p>top and bottom rankings;</p></li>
<li><p>above- and below-average rules;</p></li>
<li><p>unique and duplicate values;</p></li>
<li><p>two- and three-color scales;</p></li>
<li><p>data bars;</p></li>
<li><p>icon sets;</p></li>
<li><p>custom expressions.</p></li>
</ul>

<h4>Start with One Line</h4>
<p>The convenience methods make common formatting rules deliberately small. To highlight every value below 100 in the <code>Quota %</code> column:</p>

<pre class="delphi" name="code">with TMSFNCDataGrid1.ConditionalFormatting.AddCellValueRule(
  COL_QUOTA, gfcLess, &#39;100&#39;) do
  Appearance.Font.Color := gcRed;</pre>

<p><code>ConditionalFormatting.Enabled</code> is enabled by default, so the rule takes effect immediately.</p>

<h4>Color Scales: See the Range</h4>
<p>A color scale turns a numeric column into a compact heat map. The lowest, middle and highest values receive the selected colors, with the colors in between calculated automatically.</p>

<pre class="delphi" name="code">TMSFNCDataGrid1.ConditionalFormatting.AddColorScale(
  COL_SALES,
  gcRed,
  gcYellow,
  gcLimegreen);</pre>

<p>This is useful for sales, margins, temperatures, scores or any other measurement where relative position matters as much as the number itself.</p>

<h4>Data Bars and Icon Sets: Compare at a Glance</h4>
<p>Data bars show magnitude directly inside a cell while keeping the original value visible. Icon sets divide values into recognizable bands using arrows, traffic lights, stars, flags and rating symbols.</p>

<pre class="delphi" name="code">// Compare quota attainment with proportional bars.
TMSFNCDataGrid1.ConditionalFormatting.AddDataBar(
  COL_QUOTA, gcDodgerblue);

// Show the direction of growth with traffic lights.
TMSFNCDataGrid1.ConditionalFormatting.AddIconSet(
  COL_GROWTH, gisTrafficLights3);

// Turn customer satisfaction into a five-star signal.
TMSFNCDataGrid1.ConditionalFormatting.AddIconSet(
  COL_CSAT, gisStars5);</pre>

<p>The minimum and maximum values can be detected automatically or defined as numbers, percentages or percentiles. Values can remain visible next to the bar or icon, or the visual can stand on its own.</p>

<h4>Highlight the Rows That Matter</h4>
<p>Some conditions should draw attention to a complete record. <code>ApplyToEntireRow</code> extends the appearance of a matching rule across the row while the condition itself remains tied to one column.</p>

<pre class="delphi" name="code">with TMSFNCDataGrid1.ConditionalFormatting.AddTopBottomRule(
  COL_SALES, rkTop, 5) do
begin
  Priority := 10;
  ApplyToEntireRow := True;
  Appearance.Fill.Color := gcLightgoldenrodyellow;
  Appearance.Font.Style := [TFontStyle.fsBold];
end;</pre>

<p>This example highlights the five best sales results without calculating or updating a threshold manually.</p>

<h4>Expressions Across Columns</h4>
<p>For decisions that depend on more than one value, expression rules use the same expression language as the Data Grid filter builder. Column names can be combined into a readable condition that is evaluated for each row.</p>

<pre class="delphi" name="code">with TMSFNCDataGrid1.ConditionalFormatting.AddExpressionRule(
  &#39;([Growth %] &lt; 0) AND ([Quota %] &gt;= 100)&#39;) do
begin
  ApplyToEntireRow := True;
  Appearance.Fill.Color := gcMistyrose;
  Appearance.Font.Color := gcRed;
end;</pre>

<p>This makes it easy to reveal combinations that deserve attention: growth declining despite reaching quota, stock falling below the reorder level, an overdue task that is still open, or a customer whose health indicators disagree.</p>

<h4>Combining Rules with Priority</h4>
<p>Conditional formatting becomes especially useful when several rules work together. A sales column can use a color scale, a quota column can show data bars, growth can display an icon set, and status can apply text-based colors&#151;all in the same grid.</p>

<p>When rules overlap, a lower <code>Priority</code> number has higher precedence. The first matching rule claims the appearance aspects it sets, while later rules can still contribute aspects that remain unset. <code>StopIfTrue</code> can stop evaluation of the rules that follow it.</p>

<p>This gives you predictable layering without moving presentation logic into paint handlers.</p>

<h4>A Visual Editor for VCL and FireMonkey</h4>
<p>Not every formatting rule needs to start in code. TMS FNC Data Grid 2.0 includes a visual conditional-formatting editor for VCL and FireMonkey at design time.</p>

<p><img src="https://www.tmssoftware.com/site/img/tmsfncuipack_7_3/conditional-formatting-editor.png" style="max-width: 100%;" data-filename="conditional-formatting-editor.png" alt="Visual conditional-formatting editor for TMS FNC Data Grid 2.0 with a populated rule list and live preview"></p>

<p>The editor lets you:</p>

<ul>
<li><p>add, duplicate, delete and reorder rules;</p></li>
<li><p>choose a rule type and its scope;</p></li>
<li><p>select columns by name;</p></li>
<li><p>configure comparisons, values and expressions;</p></li>
<li><p>select fills, text colors, borders and font styles;</p></li>
<li><p>configure color-scale bounds, data bars and icon sets;</p></li>
<li><p>preview the selected rule before accepting it.</p></li>
</ul>

<p>The rule collection is stored with the grid, so design-time configuration becomes part of the form just like the grid&#39;s other properties. Desktop applications can also open the same workflow at runtime with <code>ShowConditionalFormattingEditor</code>.</p>

<h4>Cross-Framework by Design</h4>
<p>The conditional-formatting engine is available in <code>TTMSFNCDataGrid</code> for <strong>VCL</strong>, <strong>FireMonkey</strong> and <strong>TMS WEB Core</strong>. The visual editor is available for VCL and FireMonkey, while the same rule collection and fluent helper methods can be configured in code on all three frameworks.</p>

<p>That means the formatting logic can travel with your application instead of being rewritten for each UI framework.</p>

<h4>Installing TMS FNC UI Pack 7.3</h4>
<p>TMS FNC UI Pack 7.3 is available through the modern Smart Setup based installation workflow:</p>

<pre class="delphi" name="code">tms self-update
tms install tms.fnc.*</pre>

<p>If you are moving from the classic installers, uninstall older TMS FNC UI Pack installations from the previous setup flow first. This keeps IDE paths and package registrations clean.</p>

<p>You can learn more about <a href="https://www.tmssoftware.com/site/tmsfncuipack.asp" target="_blank">TMS FNC UI Pack</a> on the product page.</p>

<h4>Conclusion</h4>
<p>Conditional formatting in TMS FNC Data Grid 2.0 turns visual data analysis into a reusable part of the grid configuration. From one-line threshold rules to color scales, rankings, icon sets and expressions across multiple columns, the new engine makes important values stand out without custom painting code.</p>

<p>Together with the visual editor and the fluent runtime API, TMS FNC UI Pack 7.3 gives developers a faster way to build grids that do more than display data&#151;they explain it.</p>

		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Embarcadero Conference Brazil - ECON26]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2512</link>
		<author>Aaron Decramer</author>
		<pubDate>Thu, 20 Aug 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
	<div style="width: 100%; color: #1a1a1a; line-height: 1.6;">

  
   	<p><span style="background-color: #12233f; color: white; padding: 6px 12px; text-align: center; border-radius: 6px; font-size: 1.35em;">Event</span>   
      
      
      
  </p><p style="margin:0 0 18px 0;"><img src="https://www.tmssoftware.com/site/img/blog/econ26.png" style="" alt="TMS Software Delphi  Components "><br></p><p style="margin:0 0 18px 0;">On September 1st, 2026, the Delphi community in Brazil comes together at the Amcham Business Center in S&#227;o Paulo for ECON26. If you spend your days developing, modernizing, or maintaining Delphi applications, or if you&#39;re the one making the calls on enterprise systems and technology direction, this is a day worth setting aside.</p>

  <p style="margin:0 0 32px 0;">The program runs across six stages, with five tracks in the main arena and a stage dedicated entirely to DevOps Best Practices. The format is built to feel less like a lecture hall and more like a day of exchange, with a gamified structure that carries you from one session to the next, and plenty of room to meet people from across the country working on the same kinds of problems you are.</p>

  <h3 style="font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: #009EE2; margin: 0 0 16px 0;">Sessions with TMS Software Products</h3>

  <!-- Session card 1 -->
  <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; background-color:#ffffff; border:1px solid #e6e6e6; border-radius:10px; margin:0 0 20px 0;">
    <tbody><tr>
      <td style="padding:24px;">

        <span style="display:inline-block; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:#009EE2; background-color:#e6f6ff; padding:4px 10px; border-radius:100px; margin-bottom:12px;">AI &amp; Innovation Track</span>

        <div style="font-size:18px; font-weight:700; line-height:1.35; margin:4px 0 14px 0; color:#111;">AI Beyond Prompts: Understanding MCP, Tool Calling, Subagents, Skills, Slash Commands, Spec-Driven Development, Harness, and More</div>

        <table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 14px 0;">
          <tbody><tr>
                  <td style="padding-right:12px;"><img src="https://www.tmssoftware.com/site/img/blog/cesar-cardoso.jpg" alt="C&#233;sar Cardoso" width="44" height="44" style="width:44px;height:44px;border-radius:50%;display:block;object-fit:cover;"></td>
            <td style="font-size:14px; color:#444;"><strong>C&#233;sar Cardoso</strong><br><span style="color:#888;">Code4Delphi</span></td>
          </tr>
        </tbody></table>

        <p style="margin:0 0 18px 0; font-size:14.5px; color:#333;">With the evolution of Artificial Intelligence, several terms, techniques, and approaches are emerging, such as MCP, Tool/Function Calling, Skills, Agents, Subagents, Slash Commands, Spec-Driven Development, Harness, among others. In this presentation, we will unravel these concepts, understanding what they are, what they are for, and where each one fits into the modern AI ecosystem. We will also see practical examples of how these techniques work and how they can be used in everyday life, both to develop better and faster and to create smarter and more integrated AI applications, always connecting everything to the Delphi ecosystem.</p>

        <div style="border-top:1px solid #eee; padding-top:18px;">
          <div style="font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:0.6px; color:#999; margin:0 0 8px 2px;">Featured in this session</div>
          <a href="https://www.tmssoftware.com/site/tmsaistudio.asp" style="text-decoration:none; color:inherit; display:block;">
            <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; background-color:#ffffff; border:1px solid #d8edf9; border-radius:10px; box-shadow:0 1px 3px rgba(0,158,226,0.08);">
              <tbody><tr>
                <td style="padding:14px; width:56px;">
                  <table role="presentation" cellpadding="0" cellspacing="0" style="width:48px; height:48px; background-color:#f0f9ff; border-radius:10px;">
                    <tbody><tr><td style="width:48px; height:48px; text-align:center; vertical-align:middle;"><img src="https://www.tmssoftware.com/site/img/blog/tms-ai-studio-icon.png" alt="TMS AI Studio" width="30" height="30" style="width:30px;height:30px;border-radius:4px;display:inline-block;object-fit:cover;vertical-align:middle;"></td></tr>
                  </tbody></table>
                </td>
                <td style="padding:14px 4px; vertical-align:middle;">
                  <div style="font-size:15px; font-weight:700; color:#111;">TMS AI Studio</div>
                </td>
                <td style="padding:14px 16px; text-align:right; vertical-align:middle; width:36px;">
                  <table role="presentation" cellpadding="0" cellspacing="0" style="width:28px; height:28px; background-color:#009EE2; border-radius:50%;"><tbody><tr><td style="width:28px; height:28px; text-align:center; vertical-align:middle; line-height:28px; color:#ffffff; font-size:15px;">&#39;</td></tr></tbody></table>
                </td>
              </tr>
            </tbody></table>
          </a>
        </div>

      </td>
    </tr>
  </tbody></table>

  <!-- Session card 2 -->
  <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; background-color:#ffffff; border:1px solid #e6e6e6; border-radius:10px; margin:0 0 32px 0;">
    <tbody><tr>
      <td style="padding:24px;">

        <span style="display:inline-block; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:#009EE2; background-color:#e6f6ff; padding:4px 10px; border-radius:100px; margin-bottom:12px;">Modernization &amp; Database Track</span>

        <div style="font-size:18px; font-weight:700; line-height:1.35; margin:4px 0 14px 0; color:#111;">Dependencies, Versioning, and Builds: A New Paradigm</div>

        <table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 14px 0;">
          <tbody><tr>
            <td style="padding-right:12px;"><img src="https://www.tmssoftware.com//site/img/blog/wagner-landgraf.jpg" alt="Wagner Landgraf" width="44" height="44" style="width:44px;height:44px;border-radius:50%;display:block;object-fit:cover;"></td>
            <td style="font-size:14px; color:#444;"><strong>Wagner Landgraf</strong><br><span style="color:#888;">TMS Software</span></td>
          </tr>
        </tbody></table>

        <p style="margin:0 0 18px 0; font-size:14.5px; color:#333;">Modern Delphi projects require more than just writing code. Managing dependencies, controlling versions, and automating builds has become fundamental to increasing productivity and ensuring reproducible environments. In this talk, we&#39;ll see how to modernize the Delphi development workflow through dependency automation, versioning, and build processes, both on workstations and in CI/CD environments. A practical approach to reduce manual configurations, avoid environment problems, and make your projects easier to maintain and evolve.</p>

        <div style="border-top:1px solid #eee; padding-top:18px;">
          <div style="font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:0.6px; color:#999; margin:0 0 8px 2px;">Featured in this session</div>
          <a href="https://www.tmssoftware.com/site/products.asp?t=dt" style="text-decoration:none; color:inherit; display:block;">
            <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; background-color:#ffffff; border:1px solid #d8edf9; border-radius:10px; box-shadow:0 1px 3px rgba(0,158,226,0.08);">
              <tbody><tr>
                <td style="padding:14px; width:56px;">
                  <table role="presentation" cellpadding="0" cellspacing="0" style="width:48px; height:48px; background-color:#f0f9ff; border-radius:10px;">
                    <tbody><tr><td style="width:48px; height:48px; text-align:center; vertical-align:middle;"><img src="https://www.tmssoftware.com/site/img/blog/tms-biz-icon.png" alt="TMS BIZ" width="30" height="30" style="width:30px;height:30px;border-radius:4px;display:inline-block;object-fit:cover;vertical-align:middle;"></td></tr>
                  </tbody></table>
                </td>
                <td style="padding:14px 4px; vertical-align:middle;">
                  <div style="font-size:15px; font-weight:700; color:#111;">TMS BIZ products</div>
                </td>
                <td style="padding:14px 16px; text-align:right; vertical-align:middle; width:36px;">
                  <table role="presentation" cellpadding="0" cellspacing="0" style="width:28px; height:28px; background-color:#009EE2; border-radius:50%;"><tbody><tr><td style="width:28px; height:28px; text-align:center; vertical-align:middle; line-height:28px; color:#ffffff; font-size:15px;">&#39;</td></tr></tbody></table>
                </td>
              </tr>
            </tbody></table>
          </a>
        </div>

      </td>
    </tr>
  </tbody></table>

  <h3 style="font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: #009EE2; margin: 36px 0 16px 0;">More Info &amp; Tickets</h3>

  <p style="margin:0 0 20px 0;">Whether you&#39;re coming for the AI talks, the DevOps track, or simply to spend a day with people who understand exactly what you&#39;re working on, ECON26 is shaping up to be worth the trip.</p>

  <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; background-color:#ffffff; border:1px solid #e6e6e6; border-radius:10px; margin:0 0 20px 0;">
    <tbody><tr>
      <td style="padding:22px 24px;">
        <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;">
          <tbody><tr>
            <td style="padding:0 0 16px 0; border-bottom:1px solid #f0f0f0;">
              <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;">
                <tbody><tr>
                  <td style="width:36px; vertical-align:middle;">
                    <table role="presentation" cellpadding="0" cellspacing="0" style="width:32px; height:32px; background-color:#e6f6ff; border-radius:50%;">
                      <tbody><tr><td style="width:32px; height:32px; text-align:center; vertical-align:middle; font-size:15px;">...</td></tr>
                    </tbody></table>
                  </td>
                  <td style="padding-left:12px; vertical-align:middle; font-size:14.5px; color:#222;"><strong>September 1st, 2026</strong></td>
                </tr>
              </tbody></table>
            </td>
          </tr>
          <tr>
            <td style="padding:16px 0; border-bottom:1px solid #f0f0f0;">
              <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;">
                <tbody><tr>
                  <td style="width:36px; vertical-align:middle;">
                    <table role="presentation" cellpadding="0" cellspacing="0" style="width:32px; height:32px; background-color:#e6f6ff; border-radius:50%;">
                      <tbody><tr><td style="width:32px; height:32px; text-align:center; vertical-align:middle; font-size:15px;">📍</td></tr>
                    </tbody></table>
                  </td>
                  <td style="padding-left:12px; vertical-align:middle; font-size:14.5px; color:#222;"><strong>Amcham Business Center, S&#227;o Paulo</strong></td>
                </tr>
              </tbody></table>
            </td>
          </tr>
          <tr>
            <td style="padding:16px 0 0 0;">
              <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;">
                <tbody><tr>
                  <td style="width:36px; vertical-align:middle;">
                    <table role="presentation" cellpadding="0" cellspacing="0" style="width:32px; height:32px; background-color:#e6f6ff; border-radius:50%;">
                      <tbody><tr><td style="width:32px; height:32px; text-align:center; vertical-align:middle; font-size:15px;">🎧</td></tr>
                    </tbody></table>
                  </td>
                  <td style="padding-left:12px; vertical-align:middle; font-size:14.5px; color:#222;"><strong>6 stages &#183; 40+ lectures &#183; 9 hours of content</strong></td>
                </tr>
              </tbody></table>
            </td>
          </tr>
        </tbody></table>
      </td>
    </tr>
  </tbody></table>

  <table role="presentation" cellpadding="0" cellspacing="0" style="width:100%; background: linear-gradient(135deg, #12233f 0%, #234a88 100%); border-radius:12px; margin:0 0 8px 0;">
    <tbody><tr>
      <td style="padding:28px; text-align:center;">
        <p style="margin:0 0 6px 0; color:#ffffff; font-size:19px; font-weight:700;">Get your ticket for ECON26</p>
        <p style="margin:0 0 20px 0; color:#c9dcf5; font-size:14px;">Full program, speaker list, and registration on the official site.</p>
        <a href="https://embarcaderoconference.com.br/" style="display:inline-block; background-color:#ffffff; color:#12233f; text-decoration:none; padding:13px 32px; border-radius:6px; font-weight:700; font-size:15px;">Tickets &amp; Full Program &#39;</a>
      </td>
    </tr>
  </tbody></table>

</div>
	
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Filtering in Delphi: From Filtering Data to Filtering Properties]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2508</link>
		<author>Gjalt Vanhouwaert</author>
		<pubDate>Thu, 20 Aug 2026 12:00:00 +0000</pubDate>
		<description><![CDATA[<P>﻿
		
		
		
		
		
		<p>Throughout this series, we&#39;ve looked at different ways of building and applying filters.</p>

<p>We started with the <b>TTMSFNCFilterBuilder</b>, moved on to visual filter dialogs, and then used <b>TTMSFNCFilterView</b> to let users build filters naturally through controls in the application.</p>

<p>Until now, the result of a filter was mainly used to answer one question:</p>

<p><b>Does this item match the filter?</b></p>

<p>With the new <b>TTMSFNCFilterRulesManager</b>, we can take that answer and do something with it.</p>

<p>Instead of only filtering data, a matching expression can now change a property, show or hide controls, enable or disable functionality, or execute your own application logic.</p><p style="text-align: center; "><img src="https://www.tmssoftware.com/site/img/blog/FilterSeries/FilterRulesPlannerExample.png" style="width: 828px;" alt="TMS Software Delphi  Components tmsfnccore"><br></p>

<h3>From Filters to Rules</h3>

<p>The easiest way to understand the Filter Rules Manager is to look at a rule as three parts:</p>

<ul>
<li><b>Where?</b> - Which objects should be evaluated?</li>
<li><b>When?</b> - Which filter condition should match?</li>
<li><b>What?</b> - What should happen when it matches?</li>
</ul>

<p>For example:</p>

<pre class="delphi" name="code">For all employees
where Country = &#39;Belgium&#39;
set Visible = True</pre>

<p>Or:</p>

<pre class="delphi" name="code">For all controls
where Name starts with &#39;btn&#39;
set Visible = False</pre>

<p>The filter itself is still handled by the same filtering logic we&#39;ve introduced throughout this series.</p>

<p>The difference is that we now attach one or more <b>actions</b> to the result.</p><p style="text-align: center; "><img src="https://www.tmssoftware.com/site/img/blog/FilterSeries/FilterRulesControlExample.png" style="width: 80%;" alt="TMS Software Delphi  Components tmsfnccore"><br></p>

<h3>A First Rule</h3>

<p>Let&#39;s start with a small example.</p>

<p>Suppose we have a panel containing several controls and only want controls whose name starts with <code>btn</code> to be visible.</p>

<p>With the Filter Rules Manager, this can be expressed in one rule:</p>

<pre class="delphi" name="code">mgr.AddRuleStartsWith(
  &#39;ShowActionButtons&#39;,
  pnlControls,
  &#39;Controls[*]&#39;,
  &#39;Name&#39;,
  &#39;btn&#39;,
  False
).AddVisibilityAction;</pre>

<p>There is quite a lot happening in only a few lines, so let&#39;s break it down.</p><p><br></p>

<h3>The Source</h3>

<p>Every rule starts with a <b>Source</b>.</p>

<p>The source is the object from which the Filter Rules Manager starts looking for the items it should process.</p>

<p>In our example, that is:</p>

<pre class="delphi" name="code">pnlControls</pre>

<p>But the source itself does not necessarily have to be the object we want to change.</p>

<p>Instead, we can navigate from the source to other objects in the source using an <b>ItemsPath</b>.</p><p><br></p>

<h3>The ItemsPath</h3>

<p>For our panel example, the ItemsPath is:</p>

<pre class="delphi" name="code">Controls[*]</pre>

<p>This tells the Filter Rules Manager to process all controls belonging to the panel.</p>

<p>The <code>*</code> indicates that every item in the collection should be evaluated.</p>

<p>So our rule now effectively says:</p>

<pre class="delphi" name="code">For every control inside pnlControls...</pre>

<p>If the ItemsPath is left empty, the rule can also be applied directly to the Source itself.</p>

<p>This makes rules useful for both individual objects and collections of objects.</p><p><br></p>

<h3>The Filter</h3>

<p>Next, we need to decide when the rule matches.</p>

<p>In our example, we evaluate the <code>Name</code> property:</p>

<pre class="delphi" name="code">Name starts with &#39;btn&#39;</pre>

<p>Internally, the rule uses <b>TTMSFNCFilterBuilder</b> to evaluate this expression.</p>

<p>This is important because it means the Filter Rules Manager isn&#39;t introducing another expression language.</p>

<p>The same operators and structured filtering concepts from the earlier parts of this series are reused here.</p>

<p>You can create a rule using one of the helper methods, as we did above, or provide filter text directly.</p>

<pre class="delphi" name="code">Rule.FilterText := &#39;Name LIKE &#39;&#39;btn%&#39;&#39;&#39;;</pre>

<p>More complex expressions can also be used, including combinations of multiple conditions.</p><p><br></p>

<h3>The Action</h3>

<p>So far, we know which objects we are processing and which ones match.</p>

<p>Now comes the new part: <b>what should happen?</b></p>

<p>In our example, we added:</p>

<pre class="delphi" name="code">AddVisibilityAction</pre>

<p>This changes the <code>Visible</code> property depending on whether the item matches the filter.</p>

<p>A matching control becomes visible, while a non-matching control becomes hidden.</p>

<p>The complete rule can therefore be read almost as a sentence:</p>

<pre class="delphi" name="code">mgr.AddRuleStartsWith(
  &#39;ShowActionButtons&#39;,
  pnlControls,
  &#39;Controls[*]&#39;,
  &#39;Name&#39;,
  &#39;btn&#39;,
  False
).AddVisibilityAction;</pre>

<p><i>For all controls on this panel, find the controls whose Name starts with "btn", and set their visibility accordingly.</i></p><p><i><br></i></p>

<h3>Actions Are Not Limited to Visibility</h3>

<p><code>AddVisibilityAction</code> is a convenient shortcut, but actions are not limited to the <code>Visible</code> property.</p>

<p>An action can target another property using an RTTI path.</p>

<p>Conceptually, an action consists of:</p>

<ul>
<li>A target property</li>
<li>A value when the filter matches</li>
<li>Optionally, a value when it doesn&#39;t match</li>
<li>Optionally, a value when the rule is inactive</li>
</ul>

<p>For example:</p>

<pre class="delphi" name="code">Rule.AddAction(
  &#39;Enabled&#39;,
  True,
  False
);</pre>

<p>A matching item will have <code>Enabled</code> set to <code>True</code>, while a non-matching item will have it set to <code>False</code>.</p>

<p>There are also convenience methods for common scenarios:</p>

<pre class="delphi" name="code">Rule.AddVisibilityAction;
Rule.AddEnableAction;
Rule.AddDisableAction;</pre>

<p>And because the target is a property path, actions can go beyond simple top-level properties.</p>

<p>This opens the door to changing appearance and other properties based on filter conditions as well.</p><p><br></p>

<h3>Multiple Actions on One Rule</h3>

<p>A rule isn&#39;t limited to one action either.</p>

<p>Once an item matches, multiple properties can be changed as part of the same rule.</p>

<p>For example, a rule could make an item visible and enable it at the same time:</p>

<pre class="delphi" name="code">Rule
  .AddVisibilityAction
  .AddEnableAction;</pre>

<p>This keeps the condition in one place.</p>

<p>You don&#39;t need to evaluate the same expression several times just because multiple things should happen when it matches.</p><p><br></p>

<h3>Rules Can Be Active or Inactive</h3>

<p>Every rule has an <code>Active</code> property.</p>

<p>This gives you an easy way to switch behavior on or off.</p>

<pre class="delphi" name="code">mgr.Rules[0].Active := not mgr.Rules[0].Active;
mgr.Rules[0].Apply;</pre>

<p>When a rule is active, its filter is evaluated and the match or no-match actions are applied.</p>

<p>When it is inactive, the rule can use an <b>InactiveValue</b> instead.</p>

<p>This is useful when disabling a rule should also restore the objects to a known state.</p>

<p>So an action can effectively have three outcomes:</p>

<ul>
<li><b>Match</b> - the item matches the filter.</li>
<li><b>No match</b> - the item doesn&#39;t match.</li>
<li><b>Inactive</b> - the rule itself is disabled.</li></ul><p><br></p><ul>
</ul>

<h3>Rules on Your Own Objects</h3>

<p>The Filter Rules Manager is not limited to visual FNC controls.</p>

<p>One of the more interesting use cases is applying rules to your own Delphi objects.</p>

<p>Suppose we have an employee object:</p>

<pre class="delphi" name="code">TEmployee = class(TComponent)
private
  FName: string;
  FCompany: string;
  FCity: string;
  FCountry: string;
  FVisible: Boolean;
published
  property Name: string read FName write FName;
  property Company: string read FCompany write FCompany;
  property City: string read FCity write FCity;
  property Country: string read FCountry write FCountry;
  property Visible: Boolean read FVisible write FVisible;
end;</pre>

<p>We can then create a rule that evaluates these objects and writes the result directly to their <code>Visible</code> property.</p>

<pre class="delphi" name="code">FManager
  .AddRuleStartsWith(
    &#39;NameFilter&#39;,
    FEmployees,
    &#39;Items[0..*]&#39;,
    &#39;Name&#39;,
    &#39;A&#39;
  )
  .AddVisibilityAction;</pre>

<p>When the rule is applied, the manager iterates over the employees, evaluates the <code>Name</code> property and updates <code>Visible</code>.</p>

<p>Your application can then decide how that property is used.</p>

<p>A grid could display only visible employees, for example, without the Filter Rules Manager needing to know anything about that grid.</p><p><br></p>

<h3>Dynamic Rules with Placeholders</h3>

<p>So far, the values in our rules have been fixed.</p>

<p>But application rules are often based on values that change at runtime.</p>

<p>This is where <b>placeholders</b> become useful.</p>

<p>A placeholder connects part of a rule to a property on another object.</p>

<p>For example, suppose we have a search edit:</p>

<pre class="delphi" name="code">FManager.AddPlaceholder(
  &#39;SearchText&#39;,
  EditSearch,
  &#39;Text&#39;
);</pre>

<p>We can then use that value inside the rule:</p>

<pre class="delphi" name="code">{SearchText}</pre>

<p>Every time the rule is applied, the current value of <code>EditSearch.Text</code> is used.</p>

<p>This means the rule doesn&#39;t have to be recreated whenever the search text changes.</p><p><br></p>

<h3>Even the Field Can Be Dynamic</h3>

<p>Placeholders are not limited to filter values.</p>

<p>They can also be used in other parts of the rule, including the field being filtered.</p>

<p>For example, imagine a ComboBox where the user selects whether to search by Name, Company, City or Country.</p>

<p>We can first retrieve the selected index:</p>

<pre class="delphi" name="code">FManager.AddPlaceholder(
  &#39;SearchFieldComboIndex&#39;,
  SearchFieldCombo,
  &#39;ItemIndex&#39;
);</pre>

<p>Then use that index to retrieve the selected field:</p>

<pre class="delphi" name="code">FManager.AddPlaceholder(
  &#39;FilterField&#39;,
  SearchFieldCombo,
  &#39;Items[{SearchFieldComboIndex}]&#39;
);</pre>

<p>And finally use both placeholders in the rule:</p>

<pre class="delphi" name="code">FManager
  .AddRuleStartsWith(
    &#39;EmployeeFilter&#39;,
    FEmployees,
    &#39;Items[0..*]&#39;,
    &#39;{FilterField}&#39;,
    &#39;{SearchText}&#39;
  )
  .AddVisibilityAction;</pre>

<p>The same rule can now search different properties without being rebuilt.</p>

<p>Change the ComboBox, enter another search value, call <code>Apply</code>, and the current values are resolved automatically.</p><p><br></p>

<h3>Applying the Rules</h3>

<p>Once the rules have been configured, they can be applied through the manager:</p>

<pre class="delphi" name="code">FManager.Apply;</pre>

<p>The manager processes its rules and, for each rule:</p>

<ul>
<li>Resolves the source and item path.</li>
<li>Resolves any placeholders.</li>
<li>Builds the filter.</li>
<li>Evaluates the filter against every item.</li>
<li>Determines whether the item matches.</li>
<li>Applies the corresponding actions.</li>
</ul>

<p>A rule can also be applied individually:</p>

<pre class="delphi" name="code">FManager.Rules[0].Apply;</pre>

<p>This is useful when only one particular rule needs to be refreshed.</p><p><br></p>

<h3>What If an Action Is Not a Property?</h3>

<p>Not every action can be expressed as setting a property.</p>

<p>Sometimes you need to execute application-specific code.</p>

<p>For those situations, rules also support callbacks.</p>

<p>The callback receives the rule, the current item, its index and whether it matched.</p>

<p>This gives you the structured rule evaluation while leaving the final behavior completely up to your application.</p>

<p>There are also manager events available when you need more control over whether a rule or item should be processed, or when you want to override the resulting match.</p>

<p>For most common cases, however, property actions keep the implementation much simpler and avoid unnecessary event handlers.</p><p><br></p>

<h3>Counting Matches Without Applying Actions</h3>

<p>Sometimes you only want to know how many items currently match a rule.</p>

<p>For that, a rule provides <code>CountMatchingItems</code>.</p>

<pre class="delphi" name="code">Count := Rule.CountMatchingItems;</pre>

<p>This evaluates the filter without writing the action values to the objects.</p>

<p>It can be useful for previews, counters or showing the user how many items a rule will affect before applying it.</p><p><br></p>

<h3>Why a Filter Rules Manager?</h3>

<p>Of course, all of this could also be written manually.</p>

<p>You could iterate over a collection, write several <code>if</code> statements, change properties and repeat that logic whenever the application state changes.</p>

<p>But as the number of conditions grows, that logic quickly becomes spread throughout the application.</p>

<p>The Filter Rules Manager brings those pieces together:</p>

<ul>
<li>The condition is represented by a filter.</li>
<li>The objects to evaluate are defined by a source and item path.</li>
<li>The result is connected to one or more actions.</li>
<li>Runtime values can be supplied through placeholders.</li>
<li>The complete rule can be enabled, disabled and reapplied.</li>
</ul>

<p>Instead of writing:</p>

<pre class="delphi" name="code">if Employee.Country = &#39;Belgium&#39; then
begin
  Employee.Visible := True;
  Employee.Enabled := True;
end
else
begin
  Employee.Visible := False;
  Employee.Enabled := False;
end;</pre>

<p>The condition and its effects become a reusable rule.</p><p><br></p>

<h3>Where Can This Be Used?</h3>

<p>The Filter Rules Manager isn&#39;t specifically a grid filtering component or a UI visibility component.</p>

<p>It is deliberately more generic.</p>

<p>Some examples include:</p>

<ul>
<li>Show or hide controls depending on application state.</li>
<li>Enable actions only when certain conditions are met.</li>
<li>Change the appearance of matching items.</li>
<li>Apply rules to collections of your own objects.</li>
<li>Highlight records requiring attention.</li>
<li>Control which application actions are currently available.</li>
<li>Apply multiple property changes from a single condition.</li>
</ul>

<p>And because the logic is based on the same filtering foundation used throughout this series, you don&#39;t need to learn another way of describing conditions.</p><p><br></p>

<h3>From Filtering Data to Application Logic</h3>

<p>This is ultimately the idea behind <b>TTMSFNCFilterRulesManager</b>.</p>

<p>A filter doesn&#39;t necessarily have to mean:</p>

<p><i>"Remove everything that doesn&#39;t match."</i></p>

<p>It can simply describe a condition:</p>

<p><i>"When this matches, do this."</i></p>

<p>By separating the condition from the action, the same structured filtering logic can be used in many more places throughout an application.</p>

<p>And because rules can work with component properties as well as your own objects, the Filter Rules Manager isn&#39;t tied to one particular control or use case.</p>

<p>It takes the FilterBuilder we&#39;ve been using throughout this series and turns it into a more general rules system for Delphi applications.</p>

		
		
		
		
		
		<br><br></P>]]></description>
	</item>
	<item>
		<title><![CDATA[﻿Protect your public MCP servers with authentication in TMS AI Studio 1.8.0.0]]></title>
		<link>https://www.tmssoftware.com/site/blog.asp?post=2511</link>
		<author>Bradley Velghe</author>
		<pubDate>Tue, 18 Aug 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, ChatGPT, 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>As long as that server only talks to itself on your own machine, it doesn&#39;t need to ask "who&#39;s calling?" The moment it becomes a real server instead &#151; reachable over the network, used by a colleague, or connected to from Claude Desktop on someone else&#39;s computer &#151; that question matters a lot.</p>

<p>You&#39;ve actually already answered that question dozens of times, just not for MCP. Every time you click <i>"Continue with Google"</i> or <i>"Sign in with Microsoft"</i> on some website, a login screen pops up, you approve it, and you&#39;re dropped back into the app, now connected. That familiar dance is called OAuth, and it&#39;s exactly the mechanism MCP uses to let an AI assistant connect to your server safely.</p>

<p>In this post we build that mechanism in Delphi. No prior knowledge of OAuth required &#151; we&#39;ll explain each piece in plain terms as we go, then show the real, compiling Delphi code behind it.</p>

<ul> <li>It&#39;s the <b>same well-known approach</b> used by "Sign in with Google" and every other app you&#39;ve connected an account to &#151; not something custom or proprietary.</li> <li>Turning it on for an existing MCP server is a <b>one-line switch</b>, not a rewrite.</li> <li>It&#39;s been tested <b>against real AI assistants</b> &#151; MCP Inspector, Claude Desktop and ChatGPT have all connected through this exact code.</li> </ul><p><br></p>

<h3>The two servers involved</h3>

<p>There are two separate pieces at play here, and keeping them separate is the whole point.</p>

<p>One is your actual <b>MCP server</b> &#151; the one with the tools the AI wants to call. The other is a small <b>login and permissions service</b>, whose only job is to check who someone is and hand out a token proving it. Your MCP server never checks logins itself; it just asks the login service, every time, "is this token still good, and what&#39;s it allowed to do?"</p>

<p><svg viewBox="0 0 860 260" role="img" aria-label="An AI assistant presents a token to the MCP server, which never trusts it by itself and instead asks the login service whether it&#39;s still valid, every time." style="max-width:100%;height:auto;">
  <defs>
    <marker id="oauthArrow1" 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>
    <marker id="oauthArrowGray" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#6E7681"></path></marker>
  </defs>
  <rect x="20" y="96" width="150" height="76" rx="12" fill="#ffffff" stroke="#D8D3C9" stroke-width="1.5"></rect>
  <text x="95" y="128" text-anchor="middle" font-family="Consolas,monospace" font-size="12" fill="#14181E">AI Assistant</text>
  <text x="95" y="146" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#6E7681">Claude &#183; ChatGPT</text>
  <text x="95" y="160" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#6E7681">MCP Inspector</text>

  <rect x="230" y="50" width="250" height="170" rx="12" fill="#0F141B"></rect>
  <text x="355" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="12" fill="#D7DEE6">Your MCP Server</text>
  <text x="355" y="96" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">the one with the tools</text>
  <rect x="250" y="110" width="210" height="26" rx="6" fill="#161D27" stroke="#27313D"></rect>
  <text x="355" y="127" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#9FB0C0">requires a valid token</text>
  <rect x="250" y="144" width="210" height="26" rx="6" fill="#161D27" stroke="#27313D"></rect>
  <text x="355" y="161" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#9FB0C0">checks it&#39;s meant for THIS server</text>
  <text x="355" y="200" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#6E7681">never trusts a token by itself</text>

  <rect x="560" y="50" width="250" height="170" rx="12" fill="#0F141B"></rect>
  <text x="685" y="78" text-anchor="middle" font-family="Consolas,monospace" font-size="12" fill="#D7DEE6">Login Service</text>
  <text x="685" y="96" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">a separate, small process</text>
  <rect x="580" y="110" width="210" height="26" rx="6" fill="#161D27" stroke="#27313D"></rect>
  <text x="685" y="127" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#9FB0C0">handles sign-in &amp; consent</text>
  <rect x="580" y="144" width="210" height="26" rx="6" fill="#161D27" stroke="#27313D"></rect>
  <text x="685" y="161" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#9FB0C0">answers "is this still valid?"</text>
  <text x="685" y="200" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#6E7681">issues &amp; vouches for tokens</text>

  <path d="M170,124 H228" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#oauthArrow1)"></path>
  <text x="199" y="116" text-anchor="middle" font-family="Consolas,monospace" font-size="9.5" fill="#0A5A62">"here&#39;s my token"</text>

  <path d="M480,157 H558" stroke="#0E7C86" stroke-width="2.5" fill="none" marker-end="url(#oauthArrow1)"></path>
  <text x="519" y="149" text-anchor="middle" font-family="Consolas,monospace" font-size="10" font-weight="700" fill="#0A5A62">is this valid?</text>

  <path d="M558,185 H480" stroke="#6E7681" stroke-width="1.25" fill="none" marker-end="url(#oauthArrowGray)"></path>
  <text x="519" y="203" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#6E7681">yes &#151; here&#39;s what it can do</text>

  <path d="M95,172 L95,232 L685,232 L685,222" stroke="#94A3B8" stroke-width="1.25" stroke-dasharray="4 3" fill="none" marker-end="url(#oauthArrowGray)"></path>
  <text x="390" y="248" text-anchor="middle" font-family="Consolas,monospace" font-size="9" font-style="italic" fill="#6E7681">first time only: sign in once, get a token</text>
</svg></p>
<p><i>Two separate services; your MCP server trusts nothing it hasn&#39;t just double-checked with the login service.</i></p><p><br></p>

<h3>Turning it on</h3>

<p>If you already have an MCP server built with the TMS MCP SDK, requiring a login is not a rewrite. It&#39;s a handful of lines on the transport you already created.</p>

<pre class="delphi" name="code">Transport := TTMSMCPStreamableHTTPTransport.Create(nil, 8934, &#39;/mcp&#39;);
Transport.RequireBearerAuthentication := True;
Transport.AuthorizationServers.Add(AuthorizationServerIssuer);
Transport.ResourceScopesSupported.Add(&#39;demo:read&#39;);
Transport.ResourceScopesSupported.Add(&#39;demo:write&#39;);
Transport.RequiredScopes.Add(&#39;demo:read&#39;);
Transport.OnValidateAccessToken := Handlers.ValidateAccessToken;

Server.Transport := Transport;</pre>

<p><code>RequireBearerAuthentication := True</code> is the actual switch: every request now needs a valid token attached, or it&#39;s refused. <code>AuthorizationServers</code> tells your server which login service to trust, and <code>ResourceScopesSupported</code> lists the permissions that exist (here, "read" and "write" access to a notes list) &#151; both get automatically announced to any AI client that asks, so nothing needs to be configured by hand on the client side.</p>

<p>The one piece the SDK deliberately leaves to you is <code>OnValidateAccessToken</code>: the actual check of "is this token real, and what does it allow?" Different login services hand out tokens in different formats, so that decision is yours, not the transport&#39;s.</p><p><br></p>

<h3>Checking a token: always ask, never assume</h3>

<p>Here is the demo&#39;s real implementation of that check. It calls the login service and asks it directly, rather than trying to verify the token by itself.</p>

<pre class="delphi" name="code">procedure TDemoAuthHandlers.ValidateAccessToken(Sender: TObject; const AToken, AResourceURI: string;
  var AValidation: TTMSMCPAccessTokenValidation);
var
  Http: TIdHTTP;
  ReqBody, RespBody: TStringStream;
  Json: TJSONObject;
  Audience: string;
begin
  AValidation.Valid := False;

  Http := TIdHTTP.Create(nil);
  ReqBody := TStringStream.Create(&#39;token=&#39; + TTMSMCPUtils.URLEncode(AToken));
  RespBody := TStringStream.Create;
  try
    Http.Request.ContentType := &#39;application/x-www-form-urlencoded&#39;;
    Http.Post(AuthorizationServerIssuer + &#39;/introspect&#39;, ReqBody, RespBody);

    Json := TJSONObject.ParseJSONValue(RespBody.DataString) as TJSONObject;
    if not Assigned(Json) or not Assigned(Json.GetValue(&#39;active&#39;)) or
      not (Json.GetValue(&#39;active&#39;) as TJSONBool).AsBoolean then
      Exit; // not valid - AValidation.Valid stays False

    // Make sure this token was actually meant for THIS server, not some
    // other one the same login service happens to protect.
    Audience := &#39;&#39;;
    if Assigned(Json.GetValue(&#39;aud&#39;)) then
      Audience := Json.GetValue(&#39;aud&#39;).Value;
    if Audience &lt;&gt; AResourceURI then
      Exit; // valid token, but issued for a different server - reject anyway

    AValidation.Valid := True;
    AValidation.Subject := Json.GetValue(&#39;sub&#39;).Value;
    AValidation.Scopes := Json.GetValue(&#39;scope&#39;).Value.Split([&#39; &#39;]);
  finally
    RespBody.Free; ReqBody.Free; Http.Free;
  end;
end;</pre>

<p>That middle check &#151; confirming the token was actually meant for <i>this</i> server &#151; matters more than it looks. Without it, a token handed out for some other server (that happens to share the same login service) would be accepted here too, just because it was technically "still valid". Checking who a token was meant for is what stops it from being reused somewhere it was never supposed to work.</p>

<p>Put together, every single request runs through the same short checklist before a tool ever gets called:</p>

<p><svg viewBox="0 0 860 132" role="img" aria-label="Every request is checked for a token, that token is confirmed with the login service, and it&#39;s confirmed to be for this server, before a tool runs." style="max-width:100%;height:auto;">
  <defs>
    <marker id="oauthArrow2" 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">request comes in</text>
  <text x="78" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">with a token</text>
  <path d="M142,67 H176" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#oauthArrow2)"></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">read the token</text>
  <text x="248" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">is one even attached?</text>
  <path d="M318,67 H352" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#oauthArrow2)"></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">ask the login service</text>
  <text x="424" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">still valid?</text>
  <path d="M494,67 H528" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#oauthArrow2)"></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">check it fits</text>
  <text x="605" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#9FB0C0">this server, this permission</text>
  <path d="M680,67 H714" stroke="#0E7C86" stroke-width="2" fill="none" marker-end="url(#oauthArrow2)"></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">run the tool</text>
  <text x="781" y="79" text-anchor="middle" font-family="Consolas,monospace" font-size="9" fill="#CDEDEE">or refuse</text>
</svg></p>
<p><i>Every call is double-checked with the login service before a tool ever runs.</i></p><p><br></p>

<h3>What it looks like in Claude Desktop</h3>

<p>All of the above is what makes this work under the hood. Here&#39;s what it actually looks like from the other side, adding this server to Claude Desktop as a custom connector &#151; and it will look familiar, because it&#39;s the same "sign in and approve" pattern as any other app.</p>

<p>Add the connector&#39;s address and hit Connect. Approve on the sign-in screen that opens, and the connector shows connected:</p>

<table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:560px;margin:20px auto;">
  <tbody><tr>
    <td style="width:50%;padding:0 8px 0 0;text-align:center;vertical-align:top;">
      <div style="background:#0F141B;border-radius:12px;padding:22px 16px;box-shadow:0 2px 10px rgba(15,20,27,0.18);">
        <img src="https://www.tmssoftware.com/site/img/blog/claude-connector-connect.png" alt="Claude Desktop&#39;s custom connector row, showing a Connect button" style="max-width:100%;height:auto;display:block;margin:0 auto;">
      </div>
      <p style="margin:10px 0 0;"><i>Before &#151; not yet connected</i></p>
    </td>
    <td style="width:50%;padding:0 0 0 8px;text-align:center;vertical-align:top;">
      <div style="background:#0F141B;border-radius:12px;padding:22px 16px;box-shadow:0 2px 10px rgba(15,20,27,0.18);">
        <img src="https://www.tmssoftware.com/site/img/blog/claude-connector-connected.png" alt="Claude Desktop&#39;s custom connector row, now showing a checkmark for a connected server" style="max-width:100%;height:auto;display:block;margin:0 auto;">
      </div>
      <p style="margin:10px 0 0;"><i>After &#151; approved and connected</i></p>
    </td>
  </tr>
</tbody></table>

<p>In between sits the sign-in screen itself &#151; plain HTML, served straight from the Delphi backend, no client-side JavaScript involved:</p>

<div style="max-width:420px;margin:20px auto;background:#ffffff;border:1px solid #D8D3C9;border-radius:12px;padding:18px;box-shadow:0 2px 10px rgba(20,24,30,0.08);text-align:center;">
  <img src="https://www.tmssoftware.com/site/img/blog/claude-consent-page.png" alt="The demo login service&#39;s consent page, asking to approve or deny read and write permissions for Claude" style="max-width:100%;height:auto;display:block;margin:0 auto;border-radius:4px;">
</div>
<p style="text-align:center;"><i>The sign-in screen: plain server-rendered HTML, nothing exotic needed to make this part work.</i></p><p><br></p>

<p>The permissions you approve here decide what a token is <i>allowed</i> to do. Claude adds its own, separate layer of consent on top of that, per tool, before it will actually use anything:</p>

<div style="max-width:560px;margin:20px auto;background:#0F141B;border-radius:12px;padding:20px;box-shadow:0 2px 10px rgba(15,20,27,0.18);text-align:center;">
  <img src="https://www.tmssoftware.com/site/img/blog/claude-tool-permissions.png" alt="Claude Desktop&#39;s tool permissions panel, showing add_note, delete_note and list_notes each set to Needs approval" style="max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px;">
</div>
<p style="text-align:center;"><i>Claude&#39;s own tool-permission setting, independent of what you approved during sign-in.</i></p><p><br></p>

<p>Two separate checks, worth keeping apart: the sign-in step decides what a token is <i>entitled</i> to at all; Claude&#39;s own permission setting decides whether it will actually use a given tool <i>right now</i> without asking first. Approving "write" access during sign-in doesn&#39;t mean Claude will silently add a note &#151; that second decision stays entirely with Claude, on your machine.</p><p><br></p>

<h3>From demo to a real deployment</h3>

<p>Both servers ship two ways: as plain console apps you can run and read top to bottom in an afternoon, and as installable Windows services (a proper install command, a config file, a log file to check) for when you actually want one sitting on a server.</p>

<p>We ran that exact path start to finish: built as Windows services, put behind IIS on a real domain with a real certificate, and connected end to end by MCP Inspector, Claude Desktop and ChatGPT &#151; sign in, approve, and a live tool call, all the way through.</p>

<p>That last stretch &#151; going from "it works on my own machine" to "it works on a real server, for anyone" &#151; has its own sharp edges: what address the server needs to tell clients about once it&#39;s behind a proxy, a place where the sign-in step can get silently misdirected by proxy configuration, and a couple of real bugs we found and fixed along the way. We wrote all of that down separately, in detail, rather than compress it into a footnote here: see the <a href="https://download.tmssoftware.com/doc/tmsaistudio/components/Authentication/">documentation</a> for the full setup-to-production walkthrough.</p><p><br></p>

<h3>Why this matters</h3>

<ul> <li>Requiring a login is a one-line switch on the server you already have, not a rewrite of your tools.</li> <li>AI clients need no manual setup to connect &#151; the whole sign-in process is standard and already understood by MCP-aware clients, the same way "Sign in with Google" already works everywhere without you configuring anything.</li> <li>Your MCP server trusts nothing by default; every token is checked with the login service, every time, and checked against being reused on the wrong server.</li> <li>The demo login service is built to be strict by default &#151; it won&#39;t redirect anywhere it hasn&#39;t been explicitly told is safe.</li> <li>The same code that runs as a five-minute local demo is the code we actually deployed on a real server with a real domain &#151; this isn&#39;t a toy that stops working the moment it leaves your own machine.</li> </ul><p><br></p>

<h3>Get the demo</h3>

<p>All four projects &#151; the two console demos and their Windows-service counterparts &#151; are in the distribution folder under <code>Demos/OAuth Authorization Server Demo</code>, <code>Demos/OAuth Resource Server Demo</code>, <code>Demos/OAuth Authorization Server Service</code> and <code>Demos/OAuth Resource Server Service</code>.</p>

<p>For everything past "it runs on my machine" &#151; turning these into installable services, adding a certificate, and putting them behind IIS on a real domain, caveats included &#151; the <a href="https://download.tmssoftware.com/doc/tmsaistudio/components/Authentication/">documentation</a> walks through that in the order you&#39;d actually run into each decision.</p>

<p>Clone it, point a client at it, and watch the whole sign-in happen without writing a single line of client-side authentication code.</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>
	<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>
	</channel></rss>
