MCP 2026-07-28 with backwards compatibility for 2025-06-18 and 2025-11-25 - #40
Open
cmgeuze wants to merge 92 commits into
Open
MCP 2026-07-28 with backwards compatibility for 2025-06-18 and 2025-11-25#40cmgeuze wants to merge 92 commits into
cmgeuze wants to merge 92 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.
[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
The PowerShell wrappers, the Node tooling, the Inspector server list, the conformance baselines and the curl-captured HTTP goldens leave the repository. The DUnitX project covers the processor and both transports in-process; MCP_GOLDEN_RECORD=1 re-records its goldens.
Keep the DUnitX project as the repository's test tooling
The requestState key comes from the operating system random generator instead of the clock-seeded RNG, the canonical JSON encoder no longer leaks a string per object member, and a signed payload that is not an object is rejected instead of raising a cast error. An HTTP response stream refuses writes after its terminator, the stdio transport counts subscription threads before it frees what they use, releases a reserved request id when starting it fails and bounds its shutdown push, the subscriptions manager waits for its listeners, the resources manager guards its lookups with the same lock as its mutations, and the progress throttle is guarded like cancellation. Results are fully initialised on every path, an unknown registry entry is a typed exception instead of a message match, a non-JSON result is an internal error instead of a leak, a nested dispatch restores the outer context, invalid UTF-8 on stdin is detected instead of replaced, CORS headers precede a rejection and header matching compares whole tokens, a body larger than the limit is discarded instead of buffered, an introspection failure is a 401, an unusable schema pattern is a validation error, a duplicate template variable is refused, a log level is percent-encoded into its URI, and sets and integers survive serialisation intact.
Security, lifetime and correctness fixes from the review
…ecords Content blocks, protocol version checks and log redaction move onto TMCPContentBlock, TMCPProtocolVersion and TLogger; the schema generator keeps its RTTI context in a class variable; the origin parser becomes TMCPOriginParts; the tools manager and the request context tests own their helpers. Nothing raises the base Exception any more: EMCPRegistryNotFound, EMCPError.MethodNotFound and EMCPConfigurationError take its place. Functions that reported through out parameters now return a record (TMCPAuthResult, TMCPLine, TMCPCompiledTemplate) or carry a Try prefix.
Types instead of loose routines, typed exceptions and result records
MCPServer.Types carries the method names, the JSON-RPC envelope keys, the media types and the JSON member names that the whole server uses, so cacheScope, the JSON-RPC version, the event-stream media type and the wildcard scope exist once instead of in four units. Every unit that repeated a literal now names it: capability names, tool, prompt and resource names, schema keywords, ini sections and the recurring messages.
One constant per protocol value, no repeated literals
ProcessRequestEx reads the request through named steps, the request context is built per era, the schema validator validates per JSON type, the schema generator describes per type kind, the serializer converts per type kind, the subscriptions manager splits opening, waiting and closing, and the HTTP handler reads, checks and answers in named methods. TMCPServerApplication replaces the wiring that the program file repeated for each transport, and Exit with a call reads as an assignment and a return.
Shorter methods and a server application class
Every for and while body has begin and end, a boolean operator ends its line instead of starting the next, an attribute sits on its own line with a blank line between test declarations, and the comparisons whose meaning fits a name are named first.
House rules across the sources and the tests
The runner insists on assertions again, so a test that asserts nothing now fails instead of passing silently. The concurrent template read counts mismatches and asserts on the main thread, because assertions made in worker threads do not reach the runner. A golden mismatch is an assertion failure with the expected and actual text, not an exception. Registration checks describe the registered set instead of counting it, so adding a tool no longer breaks them. The progress throttle only judges the dropped burst when the burst really stayed inside the interval, and the remaining waits poll a condition through a shared helper. Stdio lookups that must find a message fail with the received lines instead of returning nil. New unit MCPServer.Tests.Support holds the shared protocol fragment, the JSON object parser and the wait helper. Test doubles expose properties instead of public fields, and unused units are gone.
test: make the suite fail on faults it could hide
Authentication and Network and Security sit under Configuration, the Commercial Support section was missing from the list, and the example sections are capitalised like the entries that point at them. The performance bullet states what the server uses instead of claiming speed.
docs: keep the README contents in step with its sections
MarkReadOnly on the three tool bases writes the readOnlyHint and openWorldHint pair a client uses to decide about parallel or auto-approved calls, instead of every tool building the annotations object itself. Calling it twice leaves one pair per hint. test_simple_text uses it, so tools/list now publishes openWorldHint: false next to its readOnlyHint; both goldens are re-recorded for it.
feat: mark a tool read-only in one call
3 tasks
The check compared two absolute paths with a plain StartsWith, so any sibling directory whose name begins with the working directory's name (mcp-server-secrets next to mcp-server) passed as if it were inside it. TPathBoundary.IsWithin accepts the base directory itself and anything below it, and nothing else. Reported by Syed Anas Mohiuddin.
…refix and traversal
…2026 fix(list_files): anchor the allowed-directory check on a path separator (2026 branch)
The conventions the file listed are visible in the code itself; the contributing section names the ones a pull request has to follow.
docs: drop the internal coding rules from the public repository
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