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
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,FRefreshTokensare all plainTDictionaryinstances 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_idfrom an earlier registration will start failing withUnknown client_idafter 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 staleclient_idunless 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 —
HandleAuthorizerejects any request missingcode_challenge/code_challenge_method=S256. -
Redirect URI validation happens before anything else.
HandleAuthorizechecks the requestedredirect_uriagainst the exact registered value for thatclient_idbefore 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 unvalidatedredirect_urito 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.
Notaction="/authorize/approve". This one line matters more than it looks — see §4.3 for why. -
The redirect target comes from the client, verbatim.
HandleApproveredirects to exactlyReq.RedirectURI— whatever was validated and stored from the original/authorizerequest. Our code never rewrites, defaults, or derives this from the server's ownIssuer. 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;
RequireBearerAuthenticationgates every request to the MCP endpoint behind a validAuthorization: Bearer <token>header, responding 401/403 per RFC 9728 / RFC 6750 when it's missing or invalid.OnValidateAccessTokenis 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/introspectendpoint overTIdHTTPfor every request.⚠️ Caveat — audience binding matters. The demo's
ValidateAccessTokenchecks that the introspected token'saudclaim 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.RequiredScopesis server-wide, not per-tool. If you wantadd_note/delete_noteto requiredemo:writewhilelist_notesonly needsdemo:read, that's a documented future extension — today, one missing scope inRequiredScopesgates every tool call on the server, uniformly.- Notes are scoped per MCP session (
Server.CurrentSessionId), not per user — two connections authenticating as the samedemo-userstill 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
TServicedescendant'sServiceCreate/ServiceDestroy/ServiceStart/ServiceStophandlers. ServiceCreateconstructs objects (server, transport, tool handlers) but does not start listening yet.ServiceStartcallsServer.Start— notServer.Run.⚠️ Caveat — this is the one mistake that will hang your service forever.
.Runis a blockingwhile Running do Sleep(1000)loop, meant for a console app's main thread to stay alive. Calling it fromServiceStartblocks 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..Startalone 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.ServiceStopcallsServer.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 withLogFile=(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:
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:
⚠️ Caveat — this needs an elevated (Administrator) prompt. Running
/installunelevated 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. IfGet-Servicedoesn't show your service afterward and nothing looked like it went wrong, this is almost always why.
Then start it from services.msc or:
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/.keyPEM 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/SSLCertFileare 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
- Get the certificate into the Windows certificate store (import a
.pfx, or use one already there). - Find its thumbprint (SHA-1 hash) — not its friendly name:
⚠️ Caveat:
netsh http add sslcertand 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. - Bind it to the port, once, ahead of time (elevated): 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:
— 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 ownUseSSL, which is typicallyFalsebehind 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):
- The initial
HttpReceiveHttpRequestcall requestedHTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, which copies small bodies inline. Separately, the code checkedHTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTSto 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. - Even after removing the inline-copy flag entirely, the remaining read loop
still duplicated data: it looped until
HttpReceiveRequestEntityBodyreportedERROR_HANDLE_EOFor 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
HttpReceiveRequestEntityBodyin 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:
-
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. -
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. -
Verify the file you think is deployed is actually deployed. A running Windows service holds its
.exefile locked; overwriting it without fully stopping the service first can silently fail. Always check the actual file'sLastWriteTime/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.) -
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. -
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.