MCP 2026-07-28 with backwards compatibility for 2025-06-18 and 2025-11-25 - #40
Open
cmgeuze wants to merge 68 commits into
Open
MCP 2026-07-28 with backwards compatibility for 2025-06-18 and 2025-11-25#40cmgeuze wants to merge 68 commits into
cmgeuze wants to merge 68 commits into
Conversation
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.
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The server serves MCP 2026-07-28 (per-request
_meta,server/discover, stateless HTTP,resultTypeand 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 inMIGRATION.md, the full list of changes inCHANGELOG.md.Highlights, each merged through its own PR into this branch:
_metavalidation,-32020/-32021/-32022, HTTP status mapping,server/discover, cache hints, header mirroring (Mcp-Method,Mcp-Name).requestState; streamed progress and log notifications on HTTP responses;subscriptions/listenwith change notifications.[RequiresScope], host allow-list, diagnostics switch.coding-rules.mdrecords the conventions the library keeps on purpose.Verification