Skip to content

Building the TMS MCP OAuth Demos: A Complete Guide

This guide walks through building a full OAuth 2.1 / MCP Authorization test setup for TMS MCP for Delphi: a minimal Authorization Server, a Resource Server that enforces bearer-token auth, turning both into installable Windows services, and fronting them with IIS on a real domain with HTTPS. It's written from the ground up, and calls out every caveat and gotcha we actually hit doing this for real.

Four projects come out of this:

Project What it is Port (default)
Demos/OAuth Authorization Server Demo Console app, minimal OAuth 2.1 + PKCE identity provider 9000
Demos/OAuth Resource Server Demo Console app, MCP server with bearer-auth enforcement 8934
Demos/OAuth Authorization Server Service Same logic as above, as an installable Windows service configurable via .ini
Demos/OAuth Resource Server Service Same logic as above, as an installable Windows service configurable via .ini

This is a test tool, not a production identity provider. There's no real credential check (the "login" page just asks you to approve as a fixed demo user), all state is in-memory only (wiped on every restart), and only public clients (PKCE, no client secret) are supported. Don't point real users at this.


1. Architecture at a glance

MCP client obtains a token from the Authorization Server, then presents it to the Resource Server; the Resource Server never trusts the token by itself, it round-trips to the Authorization Server's introspect endpoint on every single request before honoring it.

The Authorization Server is a separate, standalone process from the Resource Server. The Resource Server never trusts a bearer token by itself — every request triggers a live RFC 7662 introspection call back to the Authorization Server's /introspect endpoint. This mirrors a real deployment where the identity provider and the MCP server are different systems.


Part 1 — The console demos

Start here. Get both console apps running locally over plain HTTP before touching services, TLS, or IIS — every layer added later is easier to debug if you know the core logic already works.

1.1 Authorization Server Demo

Single .dpr, ~750 lines, no forms. Everything is built directly on TTMSMCPHTTPServer (not the MCP transport classes — this is a bare OAuth server, not an MCP server).

Endpoints and what they do:

Endpoint Method Purpose
/.well-known/oauth-authorization-server GET RFC 8414 metadata document
/register POST RFC 7591 Dynamic Client Registration
/authorize GET PKCE authorize request → renders a consent page
/authorize/approve POST Consent form submit → redirects with an auth code
/token POST Exchanges an auth code (or refresh token) for an access token
/introspect POST RFC 7662 — the Resource Server calls this to validate tokens

Key implementation details worth understanding:

  • In-memory state only. FClients, FPendingRequests, FAuthCodes, FAccessTokens, FRefreshTokens are all plain TDictionary instances with no persistence and no expiry sweep. Every registered client, every issued code, every token lives only as long as the process does.

    ⚠️ Caveat: restarting the process (or the service) wipes all of this. Any client that cached its client_id from an earlier registration will start failing with Unknown client_id after a restart — it needs to register again. This bit us repeatedly while testing: MCP Inspector in particular caches its DCR registration client-side and will happily replay a stale client_id unless you force it to re-register (private/incognito browser window, or an explicit "disconnect"/"clear auth" action in the client).

  • PKCE with S256 is mandatory, not optional — HandleAuthorize rejects any request missing code_challenge/code_challenge_method=S256.

  • Redirect URI validation happens before anything else. HandleAuthorize checks the requested redirect_uri against the exact registered value for that client_id before doing anything else, and — critically — never redirects on a mismatch, it renders an in-page error instead. This is a deliberate security property (an attacker can't use an unvalidated redirect_uri to redirect a code in flight anywhere they choose), so don't "fix" a redirect_uri mismatch by loosening this check.

  • The consent page's form action must be path-relative, not root-relative.

    Html.Append('<form method="POST" action="authorize/approve">');
    
    Not action="/authorize/approve". This one line matters more than it looks — see §4.3 for why.

  • The redirect target comes from the client, verbatim. HandleApprove redirects to exactly Req.RedirectURI — whatever was validated and stored from the original /authorize request. Our code never rewrites, defaults, or derives this from the server's own Issuer. If a redirect ever goes somewhere unexpected, the bug is either in what was actually requested (a stale/different registered client — see above) or somewhere downstream of this code (a reverse proxy — see §4.6), not in this logic.

1.2 Resource Server Demo

Built on TTMSMCPStreamableHTTPTransport — the real MCP transport, with its OAuth 2.1 resource-server support switched on:

Transport := TTMSMCPStreamableHTTPTransport.Create(nil, 8934, '/mcp');
Transport.RequireBearerAuthentication := True;
Transport.AuthorizationServers.Add(AuthorizationServerIssuer);
Transport.ResourceScopesSupported.Add('demo:read');
Transport.ResourceScopesSupported.Add('demo:write');
Transport.RequiredScopes.Add('demo:read');
Transport.OnValidateAccessToken := Handlers.ValidateAccessToken;
  • RequireBearerAuthentication gates every request to the MCP endpoint behind a valid Authorization: Bearer <token> header, responding 401/403 per RFC 9728 / RFC 6750 when it's missing or invalid.
  • OnValidateAccessToken is where you plug in how tokens get validated — the SDK has no opinion on token format. This demo validates by calling the Authorization Server's /introspect endpoint over TIdHTTP for every request.

    ⚠️ Caveat — audience binding matters. The demo's ValidateAccessToken checks that the introspected token's aud claim matches this specific resource's canonical URI, not just that the token is "active". Skipping this check would let a token issued for a different resource be replayed here — this is the "access-token-privilege-restriction" requirement from the MCP Authorization spec, not an optional extra.

  • RequiredScopes is server-wide, not per-tool. If you want add_note/ delete_note to require demo:write while list_notes only needs demo:read, that's a documented future extension — today, one missing scope in RequiredScopes gates every tool call on the server, uniformly.
  • Notes are scoped per MCP session (Server.CurrentSessionId), not per user — two connections authenticating as the same demo-user still see separate note lists. This is just how the demo's tool-side state happens to be organized, not an OAuth concept.

1.3 Manual smoke test (do this before moving on)

# Terminal 1
OAuthAuthorizationServerDemo.exe
# Terminal 2
OAuthResourceServerDemo.exe

# Terminal 3
curl -i http://localhost:8934/mcp -X POST -H "Content-Type: application/json" -d "{}"
#   -> 401, with a WWW-Authenticate header pointing at the resource metadata
curl -i http://localhost:8934/.well-known/oauth-protected-resource
#   -> 200, the RFC 9728 Protected Resource Metadata document

If both of these don't behave exactly as commented, stop and fix it here — every later layer (services, TLS, IIS) only adds more places for a real bug to hide behind a plausible-looking symptom.


Part 2 — Turning them into Windows services

Once the console demos work, wrapping them as installable Windows services follows the same Vcl.SvcMgr.TService pattern used elsewhere in this repo (Dev/MCPServerService).

2.1 The shape of the conversion

  • Console app's global logic → moved into a TService descendant's ServiceCreate/ServiceDestroy/ServiceStart/ServiceStop handlers.
  • ServiceCreate constructs objects (server, transport, tool handlers) but does not start listening yet.
  • ServiceStart calls Server.Start — not Server.Run.

    ⚠️ Caveat — this is the one mistake that will hang your service forever. .Run is a blocking while Running do Sleep(1000) loop, meant for a console app's main thread to stay alive. Calling it from ServiceStart blocks the Service Control Manager's start callback indefinitely — Windows will eventually time out and kill the service, and it may crash-loop (restart, fail, restart) several times before you notice. .Start alone is what actually spins up the HTTP listener; .Run's loop is redundant for a service, which already has its own lifecycle managed by the SCM.

  • ServiceStop calls Server.Stop.

2.2 Configuration: use an INI file, not hardcoded constants

The console demos hardcode their port/issuer as const. A service needs to be configurable per-deployment without recompiling — an INI file next to the .exe (same base name, .ini extension), read once in ServiceCreate, is the natural fit:

[Server]
Port=9000
BindingIP=127.0.0.1
UseSSL=0
SSLCertFile=
Issuer=https://your-domain.example.com
[Logging]
LogFile=

⚠️ Caveat — TIniFile.ReadString's default only applies when the key is absent, not when it's present but empty. A template that ships with LogFile= (present, blank) will NOT fall back to your coded default — it'll return an empty string, and if that empty string gets used directly as a filename, logging silently breaks. Guard explicitly:

FLogFile := Ini.ReadString('Logging', 'LogFile', '');
if FLogFile = '' then
  FLogFile := TPath.ChangeExtension(ParamStr(0), '.log');

2.3 Logging: services have no console

Every WriteLn in the console demo becomes a file-append call instead, guarded by a critical section (HTTP requests are serviced on worker threads, and naive concurrent appends will interleave/corrupt log lines):

procedure TAuthServerHandlers.LogMessage(const Msg: string);
begin
  FLogLock.Enter;
  try
    TFile.AppendAllText(FLogFile, Format('[%s] %s' + sLineBreak,
      [FormatDateTime('yyyy-mm-dd hh:nn:ss', Now), Msg]));
  finally
    FLogLock.Leave;
  end;
end;

This log file becomes your primary debugging tool once the service is running headless on a remote server — lean on it heavily; see Part 6.

2.4 Install / uninstall

Standard TServiceApplication command-line handling, already wired up by the .dpr:

OAuthAuthorizationServerService.exe /install
OAuthAuthorizationServerService.exe /uninstall

⚠️ Caveat — this needs an elevated (Administrator) prompt. Running /install unelevated fails silently — no error, no exception, the process just exits having done nothing, because opening the Service Control Manager for write access requires admin rights. If Get-Service doesn't show your service afterward and nothing looked like it went wrong, this is almost always why.

Then start it from services.msc or:

net start "TMS MCP OAuth Authorization Server (Demo)"

2.5 Every restart wipes all state — again, louder this time

This is worth repeating because it caused real confusion during testing: registered clients, pending authorization requests, issued codes and tokens are all in-memory. Every time you stop/replace/start the service (e.g. to deploy a new build), every client that had registered is gone. Any bookmarked /authorize URL, any client with a cached client_id, will now fail with Unknown client_id — not because anything is broken, but because you're pointing at a server that has never heard of that client in its current lifetime. Always re-register (or let the client re-register itself) after restarting a service you're actively testing against.


Part 3 — HTTPS directly on Windows (http.sys)

This SDK's Windows HTTP server (TMS.MCP.HTTPServer.Win.pas) talks directly to http.sys via the native HTTP Server API — the same kernel-mode component IIS itself is built on. This has real consequences for how TLS works here, different from the more familiar Indy/OpenSSL pattern.

⚠️ Caveat — there is no code path here that loads a raw .crt/.key PEM file. A separate Indy/OpenSSL-based implementation (TMS.MCP.HTTPServer.General.pas) does accept PEM files directly, but it's only compiled in for non-Windows builds. On Windows, CertFile/SSLCertFile are consulted only as a fallback pointer to a certificate already sitting in the Windows certificate store — never as an actual certificate file.

3.1 Getting a certificate bound

  1. Get the certificate into the Windows certificate store (import a .pfx, or use one already there).
  2. Find its thumbprint (SHA-1 hash) — not its friendly name:
    Get-ChildItem Cert:\LocalMachine\My | Select-Object Thumbprint, FriendlyName, Subject
    

    ⚠️ Caveat: netsh http add sslcert and this SDK's INI-based fallback both require the thumbprint specifically. Friendly names aren't accepted anywhere in this binding path, even though IIS's own site-bindings UI happens to let you pick a certificate by friendly name — that's an IIS Manager convenience, not something http.sys itself understands.

  3. Bind it to the port, once, ahead of time (elevated):
    netsh http add sslcert ipport=0.0.0.0:9000 certhash=<THUMBPRINT> appid={00112233-4455-6677-8899-AABBCCDDEEFF}
    netsh http add urlacl url=https://+:9000/ user=Everyone
    
    Pre-binding like this means the service never needs to self-elevate at startup (see the caveat below) and there's no UAC prompt to worry about for a headless service.

3.2 Caveat: the service's own self-registration logic can silently fail

If you don't pre-bind, this SDK will try to reserve the URL ACL itself on first start, via an elevated ShellExecuteEx with lpVerb='runas' and an infinite wait on the resulting process handle. A Windows service has no interactive desktop to show a UAC prompt on — depending on the service account, this can fail outright or hang until the Service Control Manager's start timeout kills it, producing exactly the crash-loop-then-eventually-succeed pattern (Service created logged 2-3 times in quick succession before one Listening... Ready.) we saw while testing this. Pre- binding via netsh ahead of time sidesteps this failure mode entirely.

3.3 The PublicHost property — and why it exists

TTMSMCPStreamableHTTPTransport.GetServerUrl used to hardcode localhost regardless of deployment. Behind a reverse proxy (or any setup where the service's own UseSSL/port don't match what's externally visible), this leaked the wrong values straight into the Protected Resource Metadata document:

{"resource": "http://tmswebacademy.com:8934/mcp", ...}

— wrong scheme (http — the service's own internal setting, since TLS is terminated upstream) and the internal port leaking through, when the real external URL was https://www.tmswebacademy.com/mcp.

The fix: PublicHost now accepts either a bare host[:port] (for a directly internet-facing deployment, no proxy — appends this transport's own port if none is given) or a full URL with scheme (https://www.tmswebacademy.com), which is used verbatim as a complete override — exactly the form you want behind a reverse proxy:

Transport.PublicHost := 'https://www.tmswebacademy.com'; // full override
// or, for a directly-exposed server with no proxy:
Transport.PublicHost := 'mcp-demo.example.com';           // scheme from UseSSL, port appended

⚠️ Caveat: always use the full-scheme form when there's a reverse proxy in front of the service. The bare host[:port] form derives its scheme from this transport's own UseSSL, which is typically False behind a proxy (TLS is terminated upstream) — using it there reintroduces exactly the bug above.


Part 4 — Fronting with IIS as a reverse proxy

If the server already has a certificate configured for IIS, reverse-proxying through it is usually less friction than juggling http.sys certificate binding directly — IIS's site-bindings UI lets you pick a certificate by friendly name, and you get to skip all of §3 entirely (the backend services just run on UseSSL=0 over loopback).

4.1 One-time prerequisite: enable ARR proxying

Server-wide, not per-site: IIS Manager → server node → Application Request Routing Cache → Server Proxy Settings → check Enable proxy → Apply.

⚠️ Caveat: without this, a URL Rewrite rule that rewrites to an absolute http://... URL will not actually proxy the request — you'll get an error (404, 502, or IIS's own generic 503, depending on exactly what's misconfigured) instead of a forwarded response, and it's easy to mistake this for a problem with the rewrite rule itself rather than this one checkbox.

4.2 Choosing a URL layout: root-mounted vs. path-prefixed

Two real options, with a real tradeoff:

Path-prefixed (https://yoursite.com/mcp/auth/*, .../mcp/resources/*) — avoids claiming generic root paths (/token, /authorize, /register) on a site that might already use them, at the cost of needing the Authorization Server's rewrite rules to strip the prefix before forwarding (its route table only knows root-relative paths like /token) — the Resource Server avoids this by simply setting its MCPEndpoint to the full external path (/mcp/resources/mcp), so no stripping is needed on that side.

Root-mounted (https://yoursite.com/authorize, .../mcp) — much simpler rewrite rules (pure pass-through, no stripping), at the cost of claiming those generic path names at the domain root.

⚠️ Caveat — check for collisions before choosing root-mounted on a live site. /authorize, /register, /token, /introspect, /mcp, and the two .well-known/... paths all get claimed at the root. If any of those already serve something on the target site, root-mounting will silently shadow it. When in doubt, either confirm they're free first, or use a dedicated subdomain (oauth-demo.yoursite.com, bound via SNI) instead of sharing the same site's config at all — that's the zero-risk option if you're not sure.

4.3 Caveat: path-relative URLs, not root-relative

If you do go path-prefixed, this matters for more than just the rewrite rules — see §1.1's note on the consent form's action attribute. A root-relative URL (/authorize/approve) always resolves against the domain root, ignoring whatever prefix the page itself was served under. A path-relative URL (authorize/approve, no leading slash) resolves against the current directory, which correctly becomes /mcp/auth/authorize/approve when the page was loaded from /mcp/auth/authorize. This is a general web-serving footgun, not specific to OAuth — anywhere a server-rendered page contains a link/form/redirect back to itself, prefer path-relative unless you specifically want root-anchored behavior.

4.4 RFC 8414 / RFC 9728 path-insertion

When an issuer or resource identifier has a path component, the corresponding discovery document does not live nested under that path — it lives at /.well-known/oauth-authorization-server (or oauth-protected-resource) with the issuer's path appended as a suffix:

Issuer:            https://yoursite.com/mcp/auth
Metadata document: https://yoursite.com/.well-known/oauth-authorization-server/mcp/auth
                   (NOT https://yoursite.com/mcp/auth/.well-known/oauth-authorization-server)

This needs its own explicit rewrite rule — it won't fall out naturally from the "strip the prefix and forward" rule used for the other endpoints. If you root-mount instead, this whole nuance disappears (no path component on the issuer → the metadata document is just at the plain .well-known path, no insertion needed).

4.5 Example rewrite rules (root-mounted layout)

<rule name="Auth Server metadata" stopProcessing="true">
  <match url="^\.well-known/oauth-authorization-server$" />
  <action type="Rewrite" url="http://127.0.0.1:8935/.well-known/oauth-authorization-server" />
</rule>
<rule name="Auth Server endpoints" stopProcessing="true">
  <match url="^(authorize/approve|authorize|register|token|introspect)$" />
  <action type="Rewrite" url="http://127.0.0.1:8935/{R:1}" />
</rule>
<rule name="Resource Server metadata" stopProcessing="true">
  <match url="^\.well-known/oauth-protected-resource(/mcp)?$" />
  <action type="Rewrite" url="http://127.0.0.1:8934/.well-known/oauth-protected-resource{R:1}" />
</rule>
<rule name="Resource Server MCP endpoint" stopProcessing="true">
  <match url="^mcp$" />
  <action type="Rewrite" url="http://127.0.0.1:8934/mcp" />
</rule>

⚠️ Caveat: add these near the top of the site's existing <rules> collection. A broad catch-all rule earlier in the list (a SPA fallback, a CMS permalink rule) can intercept these paths first and swallow them — the rest of the site keeps working fine, but these rules would silently never fire.

4.6 Caveat: IIS can silently rewrite OAuth redirects

If ARR's "Reverse rewrite host in response headers" option is enabled (it can be set per-outbound-rule, or defaults on depending on configuration), IIS will actively rewrite Location: headers coming back from the backend — replacing any reference to the backend's own host with the site's public-facing one. This is usually exactly what you want for a normal reverse proxy (fixing up backend-internal links in HTML), but it actively breaks the OAuth redirect step: the Authorization Server's /authorize/approve redirects to the client's own callback URL (http://localhost:6274/oauth/callback, or a native app's ephemeral loopback listener) — a URL that has nothing to do with the backend at all. If IIS rewrites its host anyway, the browser ends up redirected to https://yoursite.com/oauth/callback instead of back to the actual client, and that 404s (nothing is meant to be listening at that path on your site — the OAuth client owns that URL, not your server).

Symptom to watch for: the path portion of the redirect is correct, but the host/scheme has been silently replaced with your own site's — and (this is the diagnostic step that actually nails it down) your own application code, if you log the URL immediately before calling Response.Redirect, shows the correct value. If the app logs the right URL but the browser ends up somewhere else, the corruption is happening downstream of your code — in IIS/ARR, not in your logic.

Fix: disable host rewriting for outbound response headers in the ARR/URL Rewrite configuration for this site.

4.7 Caveat: a second service claiming a port that's already in use

If two http.sys-registered listeners on the same box try to claim overlapping URL namespaces on the same port (a leftover IIS site binding on that port, another test process, etc.), the port can appear to be "listening" perfectly normally in netstat — but requests to it return IIS/http.sys's own generic canned error page:

<HTML><HEAD><TITLE>Service Unavailable</TITLE></HEAD>
<BODY><h2>Service Unavailable</h2><hr><p>HTTP Error 503. The service is unavailable.</p></BODY></HTML>

This is not your application's own error response (which would be JSON, per this codebase's conventions) — it's http.sys/IIS answering on your service's behalf before the request ever reaches your process. The tell: netstat -ano | findstr :PORT shows something listening (usually PID 4, "System" — completely normal for any http.sys listener, not itself a red flag), yet a direct call to http://127.0.0.1:PORT/... from the same machine (bypassing any reverse proxy entirely) still returns this exact page. If a direct, no-proxy-involved call to your own service's port returns this, the fix is almost always to move your service to a different, genuinely-unclaimed port — verifying the specific conflicting registration (netsh http show urlacl, IIS's own site bindings via appcmd list site) is possible but often more effort than just picking a different port.


Part 5 — SDK bugs we found and fixed along the way

These are genuine bugs in the shared SDK code (MCP/TMS.MCP.HTTPServer.Win.pas, MCP/TMS.MCP.Transport.StreamableHTTP.pas), not demo-specific issues — worth knowing about if you're building anything else on top of this Windows HTTP server.

5.1 POST request bodies were being read twice and concatenated

Symptom: a valid, well-formed JSON request body arrives at the server duplicated — {...}{...}, the exact same bytes twice with no separator — which then fails to parse as JSON (TJSONObject.ParseJSONValue chokes on the trailing garbage), surfacing as a generic "invalid request body" error with no obvious cause. This affected any POST with a body small enough to be read in a single pass — OAuth's /register, and MCP's own initialize JSON-RPC call, both hit it.

Root cause, in two parts (TMS.MCP.HTTPServer.Win.pas):

  1. The initial HttpReceiveHttpRequest call requested HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, which copies small bodies inline. Separately, the code checked HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS to decide whether to also read the body via a second API, HttpReceiveRequestEntityBody — but that flag turned out to be set independently of whether the inline copy already captured everything, so both paths fired and their results got concatenated.
  2. Even after removing the inline-copy flag entirely, the remaining read loop still duplicated data: it looped until HttpReceiveRequestEntityBody reported ERROR_HANDLE_EOF or returned 0 bytes, but an extra "confirmation" call made purely to detect end-of-body was observed to re-deliver the same bytes instead of signaling completion.

Fix: stop requesting the inline copy, and terminate the read loop as soon as a short read is observed (fewer bytes returned than the buffer size) — that's a reliable signal the body is exhausted, without needing one more (buggy) call to confirm it.

If you're extending this HTTP server implementation yourself, treat any use of HttpReceiveRequestEntityBody in a loop with suspicion unless it also checks for a short read as a termination condition — relying solely on the documented EOF/zero-byte signals isn't sufficient in practice on at least this http.sys version.

5.2 GetServerUrl hardcoded localhost

Covered in §3.3 — the fix added a PublicHost property, defaulting to empty (preserving the old localhost behavior exactly) so this is fully backward-compatible.

5.3 Root-relative URL in server-rendered HTML

Covered in §4.3 — the consent page's form action was /authorize/approve (root-relative); changed to authorize/approve (path-relative), which is correct both at the domain root and under any reverse-proxy path prefix.


Part 6 — Debugging playbook

The general method that actually cracked each of the bugs above, worth reusing for anything similar:

  1. Test the backend directly, bypassing every proxy layer. Before suspecting your own OAuth/MCP logic, hit http://127.0.0.1:<port>/... straight from the server itself. If a direct call already reproduces the bug, the problem is in your application (or the shared SDK) — not IIS, not ARR, not the client. If the direct call is fine but the public URL isn't, the problem is somewhere in the proxy chain, and you've just ruled out an entire layer of code.

  2. Add temporary, precise diagnostic logging at the exact boundary in question, rather than guessing. When we suspected a redirect URL was getting mangled, the decisive step was logging the URL immediately before calling Response.Redirect — that either confirms your code is right (pointing downstream) or wrong (pointing at your own logic), with no ambiguity. Half-measures (guessing based on symptoms alone) burned far more time than one well-placed log line.

  3. Verify the file you think is deployed is actually deployed. A running Windows service holds its .exe file locked; overwriting it without fully stopping the service first can silently fail. Always check the actual file's LastWriteTime/size on disk after "replacing" it, before spending time debugging a fix that never actually shipped. (Coincidentally-identical file sizes across builds do happen — rely on the timestamp, not just the size, to confirm a rebuild actually landed.)

  4. Remember what state is wiped by what action. Restarting a service wipes its in-memory registered clients/tokens (§2.5). Restarting doesn't clear a client's own cached registration — that lives client-side, and stale client_ids reused after a server restart produce a different, easily-confused symptom (Unknown client_id) that looks like a bug but is just a state mismatch between two independently-lived things.

  5. A generic, HTML-shaped error page is a signal about which layer answered. This SDK's own error responses are JSON. IIS's are the classic <HTML><TITLE>Service Unavailable</TITLE>... page. Which one you're looking at tells you immediately whether your application code ran at all.


Appendix — File manifest

Demos/
  OAuth Authorization Server Demo/
    OAuthAuthorizationServerDemo.dpr      — console app, all logic inline
    OAuthAuthorizationServerDemo.dproj
  OAuth Resource Server Demo/
    OAuthResourceServerDemo.dpr           — console app, all logic inline
    OAuthResourceServerDemo.dproj
  OAuth Authorization Server Service/
    AuthServerServiceMain.pas             — TService descendant + OAuth handler class
    AuthServerServiceMain.dfm
    OAuthAuthorizationServerService.dpr
    OAuthAuthorizationServerService.dproj
  OAuth Resource Server Service/
    ResourceServerServiceMain.pas         — TService descendant + MCP tool handlers
    ResourceServerServiceMain.dfm
    OAuthResourceServerService.dpr
    OAuthResourceServerService.dproj

MCP/
  TMS.MCP.HTTPServer.Win.pas              — http.sys-backed HTTP server (§5.1 fix lives here)
  TMS.MCP.Transport.StreamableHTTP.pas    — MCP transport, PublicHost (§3.3/§5.2 fix lives here)

Deployment artifacts (Release-build .exes, .ini configs, IIS rewrite-rule snippets) aren't checked into source control — they're build/deployment output, regenerated from the source above for whichever server you're targeting.