Skip to content

MCP 2026-07-28 with backwards compatibility for 2025-06-18 and 2025-11-25 - #40

Open
cmgeuze wants to merge 68 commits into
mainfrom
feature/mcp-2026-07-28
Open

MCP 2026-07-28 with backwards compatibility for 2025-06-18 and 2025-11-25#40
cmgeuze wants to merge 68 commits into
mainfrom
feature/mcp-2026-07-28

Conversation

@cmgeuze

@cmgeuze cmgeuze commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

The server serves MCP 2026-07-28 (per-request _meta, server/discover, stateless HTTP, resultType and cache hints, multi round-trip requests, streamed notifications, subscriptions/listen) and the initialize-based revisions 2025-06-18 and 2025-11-25 on the same HTTP endpoint and the same stdio process. Library consumers keep their tools, resources and managers; new behaviour is documented in MIGRATION.md, the full list of changes in CHANGELOG.md.

Highlights, each merged through its own PR into this branch:

  • Protocol core: era detection, _meta validation, -32020/-32021/-32022, HTTP status mapping, server/discover, cache hints, header mirroring (Mcp-Method, Mcp-Name).
  • Tools, resources, prompts, resource templates, completion, JSON Schema 2020-12 validation, content samples for the conformance suite.
  • stdio: LF framing without BOM, worker threads, cancellation, progress, drain on EOF.
  • Multi round-trip requests with HMAC-sealed requestState; streamed progress and log notifications on HTTP responses; subscriptions/listen with change notifications.
  • Opt-in bearer authentication with OAuth 2.1 resource-server discovery, [RequiresScope], host allow-list, diagnostics switch.
  • coding-rules.md records the conventions the library keeps on purpose.

Verification

  • DUnitX: 370/370 on Win64 and Win32, no leaks, zero hints and warnings; Linux64 builds.
  • Conformance 2026-07-28: 155 passed, no scored failures (only the Tasks extension and the pending custom-header scenario fail, neither is scored). 2025-11-25: 59 passed, the 8 remaining failures are the deprecated features this server deliberately does not serve (logging/setLevel, server-initiated sampling and elicitation, resources/subscribe).
  • HTTP goldens, Inspector smoke over legacy, auto, modern and stdio (26 tools) and the stdio smoke run pass.

cmgeuze and others added 30 commits September 3, 2026 12:02
Pins the JSON-RPC layer (37 cases through TMCPJsonRpcProcessor with the same
registry as MCPServer.dpr) and the Streamable HTTP transport (26 curl cases
against the built executable) as they behave on the unchanged 2025-06-18 code.

- tests/MCPServer.Tests.dpr: DUnitX console runner, Win32 and Win64
- tests/MCPServer.Tests.Golden.pas: golden loader, mask and shape
  normalisation, record mode via MCP_GOLDEN_RECORD=1
- build-tests.bat, scripts/run-tests.ps1, scripts/capture-http-goldens.ps1
- tests/golden/README.md documents the format, the recording procedure and
  the current defects the goldens pin (logs://recent double free, nil params)
Moves the JSON-RPC error codes to MCPServer.Types and adds the constants
the 2026-07-28 work needs: protocol revisions and version sets, the MCP
error codes -32020/-32021/-32022 (and the legacy -32002), the reserved
_meta keys and the list of cacheable methods.

MCPServer.JsonRpcProcessor keeps JSONRPC_* as aliases so consumer code
compiles unchanged; the unused duplicate block in MCPServer.IdHTTPServer
is removed. MCP_PROTOCOL_VERSION stays '2025-06-18'.

Also makes the HTTP golden comparison trim trailing newlines on both
sides (SSE bodies end with a blank line).
TServerStatusResource.IncrementRequestCount, ConnectionOpened and
ConnectionClosed run on every Indy connection thread; they now use
AtomicIncrement and a compare-and-swap loop that never goes below zero,
and GetResourceData reads the counters atomically. GetNextEventID uses
AtomicIncrement as well.

TMCPRegistry creates its dictionaries in a class constructor instead of
lazily, and documents that registration must complete before the managers
are created. SetNamePrefix documents the same constraint.

Tests: concurrent counter updates, below-zero guard, registry contents.
TMCPStdioTransport.Create now forces TLogger.UseStdErr and sets the new
TLogger.StdoutReserved guard. While the guard is set, console logging
always goes to stderr and setting UseStdErr back to False is refused with
a one-time warning on stderr. Library consumers that create the transport
themselves no longer corrupt the MCP channel with log lines.

Tests: guard forces stderr, refusal warns once, release restores the
previous behaviour, transport constructor sets the guard.
…st baselines

scripts/run-conformance.ps1 builds and starts the server and runs the
official conformance CLI for the frozen 2026-07-28 and 2025-11-25
requirement sets against the same endpoint. One expected-failures file per
set (conformance-baseline-<revision>.yml), because a scenario can pass on
one wire and fail on the other and a passing baseline entry counts as
stale. The baselines record the current state: 36 scored failures for
2026-07-28, 22 for 2025-11-25.

scripts/run-inspector-smoke.ps1 lists tools through the Inspector CLI for
the legacy, auto and modern eras over HTTP and for stdio (ci-servers.json);
legacy, auto and stdio pass, modern fails until server/discover exists.

scripts/run-stdio-smoke.ps1 drives --stdio through cmd redirection and
checks the framing: one JSON object per line on stdout, logs on stderr.
It also records that non-ASCII stdin is decoded with the ANSI code page.

package.json pins the Node tooling (conformance 0.2.0-alpha.11 for the
--requirements flag, Inspector 2.5.0).
README: library checklist (register before start, stdout rules for stdio,
server://status is opt-in, constants in MCPServer.Types), resource list
matching what the executable registers, automated-tests section.
CHANGELOG.md starts with the unreleased phase-0 entries.
…le recording

- logs://recent answered "Invalid pointer operation": the copied entries were
  owned by two lists and freed twice.
- TMCPSerializer wrote TList<T> and TObjectList<T> properties as an object
  with count and capacity; they are JSON arrays now, so project://info lists
  its features and logs://recent its entries.
- server://status was declared but never registered; the unit registers it
  by default and SetNamePrefix replaces that registration
  (TMCPRegistry.UnregisterResource is new).
- resources/read without params is handled like a missing uri, and
  tools/call without arguments hands the tool an empty object, instead of
  dereferencing nil.

Goldens re-recorded for the affected cases (resources-list,
resources-read-project-info, resources-read-logs-recent,
resources-read-server-status, resources-read-without-params,
tools-call-missing-arguments) plus the HTTP resources cases; every other
golden is unchanged.
The golden README, the fixture comment, the CHANGELOG and the baseline
comments describe what the files are, not when they were made or what
comes next. The Inspector smoke script takes the entries that must fail
as a parameter instead of assuming a fixed set.
Test harness, golden files and hygiene fixes
The processor decides the protocol era per request in BuildRequestContext:
initialize is always legacy, a params._meta with
io.modelcontextprotocol/protocolVersion is modern, everything else is
legacy (negotiated stdio revision, MCP-Protocol-Version header, or
2025-11-25). Modern requests get their _meta validated (-32602), unknown
revisions -32022 with the supported list, header/body disagreement -32020,
and the legacy-only methods -32601. Modern results carry resultType,
_meta.serverInfo and, for the cacheable methods, ttlMs and cacheScope.
The HTTP status for each outcome is computed here; the transport still
answers 200 for everything.

- MCPServer.Errors: EMCPError with code, data, HTTP status and factories
- MCPServer.RequestContext: IMCPRequestContext implementation, thread-local
  Current, TMCPTransportHints
- MCPServer.Capabilities: capabilities derived from the registered managers
- MCPServer.CoreManager: server/discover; initialize negotiates the revision,
  drops sessionId and the non-schema capability keys; no session state
- MCPServer.Types: era, request id, session slot, new interfaces, version
  helpers; managers expose IMCPCapabilityProvider; the registry enumerates
  its managers and injects itself into IMCPRegistryAware managers
- Settings: Title, Description, WebsiteUrl, Instructions, LenientModernPing,
  DiscoverListsLegacyVersions, DiscoverTtlMs
- Transports pass hints (header, stdio session) and use ProcessRequestEx;
  the HTTP server no longer scrapes sessionId from the body
- TRequestContextTests: one test per branch of BuildRequestContext
- TProcessorTests: status policy, modern envelope, discover, client
  responses, error data, thread-local context, 50 concurrent initializes
- TCapabilityBuilderTests
- tests/golden/modern: 16 cases replayed through the JSON-RPC layer;
  TGoldenRunner shared by the legacy and modern fixtures
- Legacy goldens re-recorded where the wire changed on purpose: initialize
  (negotiated revision, cleaned capabilities, no sessionId), batch arrays,
  id null, missing method or jsonrpc, params not an object, server/discover
  without _meta
- HTTP goldens: six modern cases added; initialize no longer carries an
  Mcp-Session-Id header
- Conformance baselines regenerated: 2026-07-28 goes from 32 to 87 passed
  checks; the Inspector smoke run now succeeds in the modern era as well
README: dual-era badge, feature line and the section "Protocol Versions
and Dual-Era Behaviour"; CHANGELOG entries for the protocol core.
Dual-era JSON-RPC core: server/discover and per-request _meta
…rrored headers

TMCPIdHTTPServer now runs the pipeline the transport spec describes:
Origin validation on every request (403 with a JSON-RPC body and
Vary: Origin; loopback origins on any port pass, the allow-list comes from
[Security] AllowedOrigins or [CORS] AllowedOrigins, null is refused), CORS
headers only when enabled, 404 for other paths, 204 for OPTIONS, 405 with
Allow for anything but POST, then the processor's HTTP status is answered.
Notifications and client responses get 202 with an empty body, 4xx
answers carry a JSON-RPC error body, SSE responses lose the id: line.

The server binds to loopback when Host is loopback (both 127.0.0.1 and
::1 when IPv6 is available) and to every interface otherwise; BindAddress
overrides. MaxRequestBodyBytes (413), MaxJsonDepth (400), MaxConnections
and an optional EndpointInfoPath are new settings. The OpenSSL 1.0.2
handler offers TLS 1.2 only. USE_TAURUS_TLS lives in src\MCPServer.inc.

Modern requests must mirror method and name into Mcp-Method and Mcp-Name
(MCPServer.HttpHeaders decodes the Base64 sentinel form strictly); a
missing or different header is -32020 with 400. A legacy request with an
unknown MCP-Protocol-Version header gets 400. An initialize that carries
modern _meta is a modern request and therefore an unknown method (404),
as a modern client probing the server expects.

Bodies are logged at Debug level through TLogger.RedactJson.
- THttpTransportTests drives TMCPIdHTTPServer on an ephemeral port with
  TIdHTTP: 202 empty body, 405 with Allow, 204, 404, Origin policy with
  and without CORS, CORS headers and preflight reflection, status codes
  per era, Mcp-Method and Mcp-Name validation, 413 and depth limits,
  session echo for legacy only, SSE without id line, loopback binding,
  EndpointInfoPath
- THttpHeadersTests: sentinel decoding table, Accept parsing, Origin
  policy, JSON depth scanner
- The test program is tests\MCPServerTests.dpr: a dotted program name made
  the compiler resolve the Indy unit IdHTTPServer as MCPServer.IdHTTPServer
- HTTP goldens re-recorded for the new transport, with modern header cases;
  conformance baselines regenerated (2026-07-28 goes from 87 to 110 passed
  checks, dns-rebinding-protection passes on both wires)
MIGRATION.md lists every behaviour change with what to configure; README
gains the network and security settings and the HTTP status rules;
CHANGELOG entries for the transport.
Dual-era Streamable HTTP: Origin check, status codes, mirrored headers, loopback bind
TMCPToolResult builds text, image, audio, embedded resource and resource
link content blocks with structuredContent, _meta and isError.
TMCPToolBase<T> gains ExecuteWithContext next to ExecuteWithParams and
EMCPToolError for failures reported as isError. Tools publish annotations
and icons (IMCPToolMetadata); resources publish title, size, annotations
(IMCPResourceMetadata), blob contents (IMCPBinaryResource) and cache hints
(IMCPCacheableResource). New schema attributes SchemaTitle, SchemaFormat,
SchemaMinimum and SchemaMaximum; EMCPError.UnknownTool and
EMCPError.ResourceNotFound carry data.name and data.uri.
Deserialisation rejects a missing required parameter, a wrong JSON type,
a fraction for an integer and an unknown enumeration name with an
EArgumentException naming the parameter; null counts as absent.
Serialisation writes enumerations by name, sets and dynamic arrays as
arrays, nil objects as null and TDateTime as ISO 8601.
The schema generator emits integer for integers, date-time strings for
TDateTime, enum names, typed arrays for sets, dynamic arrays and TList<T>,
nested object schemas and additionalProperties: false for parameter-less
tools.
tools/call answers -32602 with data.name for an unknown tool and for a
missing name or non-object arguments; argument validation errors and
EMCPToolError become isError results; every result has a content array
and a typed result gets a text block next to structuredContent.
resources/read answers -32002 (initialize-based) or -32602 (modern) with
data.uri for an unknown URI, -32603 for a failing read, and delivers
IMCPBinaryResource contents as blob. Lists follow registration order,
carry resource metadata and, in the modern era, ttlMs and cacheScope.
AddTool and AddResource register instances outside TMCPRegistry.
One small tool per content type (test_simple_text, test_image_content,
test_audio_content, test_embedded_resource, test_multiple_content_types,
test_error_handling) and the resources test://static-text and
test://static-binary, the fixtures the conformance suite calls.
logs://recent no longer logs its own reads; project://info names the
current protocol revisions and is cacheable for an hour.
DUnitX fixtures for TMCPToolResult, the serializer, the schema generator
and the tools and resources managers in both eras. Goldens re-recorded
for the new tools, schemas, error codes and ISO timestamps; conformance
baselines pruned of the content-block and binary-resource scenarios that
pass now, with resources-templates-read added.
Tools and resources: content blocks, validation and error codes
…lation

UTF-8 byte streams (MCPServer.StdioChannel) replace Text I/O, which decoded
stdin with the console code page and mangled non-ASCII input. A reader
thread answers notifications, client responses and legacy ping inline;
every other request goes through a queue to MaxConcurrentRequests worker
threads (default 1, so responses keep arriving in order).

notifications/cancelled stops the named request and it gets no response
(IMCPRequestContext.IsCancelled/CheckCancelled/Cancel, IMCPRequestTracker).
A request with _meta.progressToken gets notifications/progress before its
response (IMCPRequestContext.ReportProgress, monotonic and throttled).

On EOF, in-flight work drains for ShutdownDrainMs before the rest is
cancelled, so the process always exits promptly. A stdio server never
writes settings.ini next to the executable; the console-control and
signal handlers, and the debug leak report, are skipped in stdio mode.
Line framing (LF splitting, CR stripping, BOM skipping, UTF-8 decoding,
overlong and invalid-UTF-8 lines), the message writer, cancellation and
progress on the request context, and the transport end to end over
in-memory streams (handshake, UTF-8 round trip, duplicate ids, cancelled
requests, progress ordering, modern requests, shutdown draining).
test_tool_with_progress reports notifications/progress for each step and
stops when the client cancels; the tools and resources managers now also
let a cancelled request propagate instead of turning it into a protocol
error.
The smoke test now drives a progress token, a cancellation and a ping
through the real executable and checks framing, ordering, and that the
process exits promptly. Goldens re-recorded for the new sample tool.
cmgeuze and others added 30 commits September 3, 2026 17:00
…ects

Adds src\Prompts to the compiler's unit search path (build.bat,
build-tests.bat, both .dproj files) and registers PromptsManager and
CompletionManager alongside the existing managers in MCPServer.dpr, for
both the HTTP and the stdio entry point.
The harness now builds prompts and completion managers alongside tools and
resources, matching MCPServer.dpr. New fixtures for TMCPPromptMessages and
TMCPPromptBase<T>, TMCPPromptsManager, TMCPCompletionManager, resource
template matching and reading, TMCPSchemaValidator, the new schema
attributes and a hand-written tool exercising TMCPToolBase's own
validation. Registration and capability-builder tests updated for the
larger registry and the two new capabilities.
prompts/list is a real method now; the unknown-method fixtures (unit tests,
golden cases and the HTTP capture script) use a name that will never be
implemented instead.
initialize and server/discover now advertise prompts and completions;
tools-list carries json_schema_2020_12_tool; resources-templates-list
carries the two registered templates; the unknown-method goldens use the
new placeholder method name.
prompts-list, prompts-get-*, resources-templates-read, completion-complete,
caching and json-schema-2020-12 now pass on both requirement sets.
Prompts, resource templates and completion
Resource templates match without a shared TRegEx and percent-decode
without treating '+' as a space. The stdio transport no longer frees
objects a worker thread may still use after the drain timeout, and the
line reader discards an overlong line chunk by chunk instead of buffering
it. TMCPLegacySession is guarded by a lock. Origin allow-list entries
match with or without the scheme's default port. JSON string fields are
checked with IsJsonString so numbers are rejected. completion/complete
answers -32002 to legacy clients for an unknown resource. The completion
manager keeps references to its managers, the /info endpoint derives its
version list from the supported versions, an invalid SchemaDefault raises,
and two leaks in tool result serialisation are closed.

Sources carry no comments; test fixtures are discovered through RTTI.
Review fixes: thread safety, lifetimes and input validation
A tool, resource or prompt raises EMCPInputRequired with the input
requests it needs and optional state. The processor answers tools/call,
resources/read and prompts/get with an InputRequiredResult, only for the
kinds of input the client declared a capability for (-32021 otherwise),
and answers -32603 to legacy clients. On the retry it validates
inputResponses (-32602 unless an object of objects), opens the sealed
requestState (HMAC-SHA256 over state, method, parameter digest, principal
and expiry; -32602 when tampered, expired or foreign) and exposes both on
the request context. [Security] RequestStateKey and RequestStateTtlSeconds
configure the sealer.
One example tool per kind of client input, signed request state across
one and two round trips, a capability-aware tool, test_missing_capability
for the stateless scenario and test_input_required_result_prompt. Tests
cover the sealer, the input request builder and readers, and the
processor flow in both eras. The 14 input-required conformance scenarios
pass and leave the baseline.
Multi round-trip requests (InputRequiredResult)
When a request accepts text/event-stream and its handler sends a
notification, the response becomes a chunked SSE stream with the
notifications before the final JSON-RPC response and X-Accel-Buffering
off; a client that disconnects cancels the request. Requests that send
nothing are answered as before. The request context gains Log and
LogJson for notifications/message, gated by the request's logLevel.
test_logging_tool logs at every level, test_streaming_elicitation logs
and then asks for a confirmation. HTTP tests cover streamed progress,
plain JSON without the event-stream accept, logLevel gating, an
InputRequiredResult and a JSON-RPC error as the final event. The
progress scenario passes in both eras and the stateless scenario in
full; the modern baseline is empty.
Stream notifications on the HTTP response
TMCPSubscriptionsManager keeps a subscriptions/listen request open: the
acknowledgement with the honoured filter goes first, every message on
the subscription carries the subscription id, HTTP streams get an SSE
keep-alive comment, stdio runs the request on its own thread, and the
server answers the request with a completion result when it closes the
subscription at shutdown. The tools, prompts and resources managers take
a ChangeNotifier: with one assigned the modern capabilities announce
listChanged and resources.subscribe, and adding, removing or updating
notifies the subscribed clients. The managers guard their lists with a
lock so run-time changes are safe.
test_trigger_tool_change, test_trigger_prompt_change and
test_trigger_resource_change mutate the lists or report a resource as
updated. Tests cover the filter, the acknowledgement and tagging, the
filter being honoured, cancellation, keep-alives, the manager hooks and
the capability flags, the HTTP stream from acknowledgement to graceful
close, and the stdio thread with cancellation and end of input. The
stateless conformance scenario passes all 30 checks.
subscriptions/listen with change notifications
MCPServer.Authorization adds IMCPAuthorizer with a static bearer
authorizer (constant-time comparison), an abstract OAuth resource-server
base that requires the audience and expiry claims and checks scopes, and
an RFC 7662 introspection authorizer. TMCPIdHTTPServer.Authorizer gates
every request except OPTIONS and the RFC 9728 protected resource
metadata document; failures answer 401, 403 or 400 with a
WWW-Authenticate Bearer challenge that names the metadata URL. Tools can
demand a scope with [RequiresScope]; the request context exposes the
principal and its scopes and the request state sealer binds tokens to
the principal. [Auth] BearerTokens, AuthorizationServers, ResourceUri
and ScopesSupported configure the executable.
Bearer authentication for the HTTP endpoint
[Security] AllowedHosts rejects requests whose Host header is not
listed with 403, so a rebinding DNS name cannot reach a server that is
published under a public name. [Server] ExposeDiagnosticsResources=0
keeps logs://recent, logs://{level} and server://status off the
executable; TMCPResourcesManager gains RemoveResourceTemplate.
Host allow-list and diagnostics resources switch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant