Proposal: separate MCP protocol maintenance from node execution using the official C# SDK
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
- Issue type
- Refactor
- Clarity
- Needs clarification
- Activity status
- Active
- Tech stack
- csharp
- Domain
- backend-api-design, testing-qa
Research direction
Start with docs/MCP_SDK_FEASIBILITY.md and docs/evidence/official-mcp-sdk/VALIDATION.md, then inspect McpToolBridge.cs, McpHttpServer.cs, SdkBridgeProof.cs, and SdkBridgeProofTests.cs. Reproduce the recorded validation and identify the unknown HTTP disposal failure before considering the SDK boundary. Done requires explicit compatibility decisions, resolved regression risk, and validation of production adoption rather than only the isolated proof.
Written by the indexing model from the issue text.
Description
McpToolBridge currently owns both Windows node tool policy and a small MCP/JSON-RPC implementation. It hardcodes 2024-11-05, parses request IDs and notifications, routes protocol methods, formats errors, and maintains cancellation matching. Those responsibilities change for different reasons than capability registration, consent, execution approvals, and sandbox policy.
The existing bridge has a useful boundary to preserve: its live INodeCapability registry, CanHandle, NodeInvokeRequest, and ExecuteAsync. I implemented a bounded experiment to see whether the official C# SDK can own protocol dispatch without replacing that execution path.
This is a protocol-core feasibility proposal, not a completed HTTP migration or a merge-ready PR. The 32-case SDK proof passed, but candidate acceptance failed: the full Shared suite repeatedly failed an existing HTTP disposal assertion, and a reduced interaction selector also failed. The exception producer and root cause remain unknown. This unresolved regression risk is separate from the npm TLS full-build blocker.
The experiment leaves the production HttpListener and all production package references unchanged. The SDK dependency and real client/server harness live in one isolated test project.
Implementation and evidence
The fork branch contains the implementation snapshot 74aa38b7, based on 273b0182745a3093c0e09f306ca8a1fff6ef3c5a.
Immutable evidence at e694bca39c2832b3fb0bc6284c0faecff3de38fa:
- Feasibility/design guide and validation ledger.
- Ordered 28-command chronology, with exact arguments, recorded boundaries, exits, source identities, counts, failure/skip names, and original TRX hashes.
- Source/binary provenance, source normalization record, and exact 19-method/32-case inventory.
- Sanitized TRX/log archive, 76 raw-to-sanitized record hashes, and archive hash. The archive also retains three historical correction patches and two bounded diagnostic-source files.
Validation preceded the implementation commit. All six final inputs match the recorded tested working-tree hashes; committed content differs only by verified CRLF-to-LF normalization. The tested index tree is d2bf4410b0feab640e2a40a21cfd191f54f16c4c; subsequent evidence commits do not change production or test source. Post-run DLL hashes identify the retained binaries. Their application ProductVersion still names the baseline commit and must not be mistaken for final-source attestation. Overwritten intermediate DLLs and unrecorded child-shell boundaries remain explicit provenance limitations.
What changes, and what stays owned by OpenClaw?
| File | Concrete change |
|---|---|
src\OpenClaw.Shared\Mcp\McpToolBridge.cs |
Extract existing invocation/error classification into internal InvokeToolAsync; retain the legacy dispatcher calling it. Make the existing live catalog method internal. Preserve serialized-text payloads and deferred delivery diagnostics. |
src\OpenClaw.Shared\OpenClaw.Shared.csproj |
Grant only the new proof assembly internal visibility; add no production SDK reference. |
tests\OpenClaw.Mcp.Sdk.Tests\OpenClaw.Mcp.Sdk.Tests.csproj |
Reference ModelContextProtocol.Core 2.2.0 and the actual Shared project. |
SdkBridgeProof.cs |
Register SDK list/call handlers using the existing catalog and policy boundary. A test-only transport decorator completes delivery diagnostics. |
SdkBridgeProofTests.cs |
Exercise official client/server stream transports, independently written JSON-RPC transcripts, safe fake capabilities, and bounded fault injection. |
The SDK path does not forward full requests to the old JSON-RPC dispatcher, reflect arbitrary capability methods into tools, or create a second registry. OpenClaw still selects and invokes its capabilities; the SDK handles protocol messages.
No new audio tools, providers, schemas, structured results, consent, or permission models are included. The separate discovery-schema proposal #1527 could later change the existing catalog projection without adding SDK-specific metadata. These branches have not been combined or validated together.
Why Core rather than an ASP.NET rewrite?
The released SDK 2.2.0 source exposes public ITransport, StreamServerTransport, and StreamableHttpServerTransport APIs. Its official in-memory example demonstrates the supported stream composition used here. Core does not require Kestrel or a hosted-service container.
The actual restored .NET 10 proof graph resolves Core 2.2.0, AI.Abstractions 10.8.3, and Logging/DependencyInjection.Abstractions 10.0.10. Normal restore succeeded without feed recovery or a dependency workaround. Core's exercised DLL has ProductVersion 2.2.0+6fa3825973949a9c4f0cd8af344e15a8db09dc35; hashes and a sanitized testhost-resolution excerpt are retained.
ModelContextProtocol adds hosting/DI integration; ModelContextProtocol.AspNetCore adds HTTP hosting. Those are alternatives, not prerequisites for this experiment. An ASP.NET sample alone is not a reason to replace an embedded desktop host.
Any production adoption would need deliberate package unification with the independent speech proposals #1528 and #1529, which pin AI.Abstractions 10.9.0. This test-only graph does not downgrade those branches or establish combined compatibility.
Compatibility decisions are part of the work
The SDK is not a drop-in parity wrapper.
| Surface | Existing behavior | Experiment or unresolved decision |
|---|---|---|
| Initialize | Always answers 2024-11-05, including missing params | Official client/server cases negotiated each of four supported initialize revisions. Choose production version policy. |
| Unsupported future initialize | Always answers 2024-11-05 | Literal 2099-01-01 request returned -32022 with supported revisions, not fallback. |
| Request IDs | Strings and arbitrary JSON numbers, retaining original number spelling | SDK uses strings or signed int64; do not silently coerce other legacy values. |
| Unknown methods | -32601, except empty resource/prompt list probes | SDK unregistered methods return -32601; decide whether to retain those probes. |
| Unknown tools | Successful JSON-RPC result with isError=true |
Retained by this handler, rather than adopting the SDK's recommended protocol-error convention. |
| Successful result | One text block containing serialized payload | Same shape in the tested path; no structuredContent or audio blocks. |
Notification named tools/call |
Executes without responding | SDK does not dispatch notifications to request handlers. |
| Cancellation | Global ID registry, tombstones/collision handling, canceled tool-error response | SDK session cancellation suppresses the response. The proof passes requestKey:null and bypasses legacy matching; this is not global cancellation/admission parity. |
| Uncorrelatable malformed JSON | Parse-error response with null ID | Official stream transport drops it; this is not an approved HTTP policy. |
tools/list |
Registry order, full list, cursor ignored | Same catalog; no automatic pagination or list-change notification claim. |
| HTTP notification status | 204 | Standard Streamable HTTP normally uses 202. |
| HTTP response / GET | Plain JSON and a friendly GET probe | Core HTTP transport writes SSE; existing client/CLI compatibility is unverified. |
| Diagnostics | Completion after delivery; deadline/shutdown attribution | Stream decorator exercises completion; host-specific lifetime mapping still needs proof. |
The delivery dictionary is not a replacement for legacy admission or tombstones: it is populated after execution and only tracks response delivery. Each invocation receives its own RelatedTransport identity so a late response cannot complete a newer invocation with a reused ID. Three focused cases passed: cancellation after the SDK handler returns, disposal failure, and late-response/ID reuse. Only the first is a real SDK-filter interleaving; the latter two are direct fault injection. Broader race freedom remains unverified.
The unsupported-version response is preserved:
{"error":{"code":-32022,"message":"Protocol version \u00272099-01-01\u0027 is not available through the initialize handshake.","data":{"supported":["2024-11-05","2025-03-26","2025-06-18","2025-11-25"],"requested":"2099-01-01"}},"id":1,"jsonrpc":"2.0"}
The pinned ValidateInitializeRequestBoundary throws UnsupportedProtocolVersionException for that handshake. The initial fallback expectation was wrong; its failed 31/32 run and both earlier proof compile failures remain recorded. The unchanged request now asserts the actual error, exact supported list, requested value, and absent result.
The newer 2026-07-28 per-request-metadata lifecycle removes initialize. It is outside this proof; passing four initialize-era revisions does not cover it.
A bounded HTTP follow-on
The existing McpHttpServer owns loopback binding/checks, host/origin/browser rejection, bearer authentication, content type, a 4 MiB body bound, admission, deadlines, and draining shutdown. SDK adoption does not remove those responsibilities or certify their correctness.
Keeping HttpListener is possible in principle: after its gates, parse the bounded body as an SDK JsonRpcMessage and use the public StreamableHttpServerTransport.HandlePostRequestAsync with the response stream and cancellation token. Keep the tracked handler lifetime, connect actual deadlines to capability execution, decide malformed-message/status handling, and complete diagnostics after delivery.
That API writes SSE and needs a deliberate session/metadata policy. A single global SDK session would conflate clients; a session per POST would lose initialize state and cancellation. Neither is an acceptable shortcut. A production migration remains blocked on those decisions and real HTTP-boundary interoperability.
Alternatives are a custom ITransport retaining plain JSON at the cost of more application-owned transport code, an isolated ASP.NET host replacement with full boundary validation, or no migration. Keeping the current small local bridge is reasonable if residual integration complexity outweighs the protocol maintenance removed. There is no demonstrated performance or automatic security improvement here.
Actual results and unresolved acceptance
Validation ran locally on Windows x64, SDK 10.0.401/runtime 10.0.12, after the exclusive host grant. No hosted workflow ran. No account, workflow-permission, or TLS workaround was used.
Counts below are passed / failed / skipped; overlapping selections must not be summed as unique coverage.
| Check | Result |
|---|---|
| Official SDK proof, final source | 32 / 0 / 0 across exactly 19 methods, confirmed twice |
| Retained HTTP/legacy-dispatch selector | Baseline 89 / 0 / 0; candidate 89 / 0 / 0, confirmed independently |
| Full Shared baseline | 4,107 / 0 / 32 |
| Full Shared candidate, two post-ACL runs | Each 4,106 / 1 / 32; unresolved |
| Reduced process-drain/HTTP selector | Baseline 32 / 0 / 0; candidate 31 / 1 / 0; unresolved |
| Full Tray | 3,072 / 0 / 0 after existing runtime-copy recovery; initial 3,071 / 1 / 0 retained |
| Full WinNode CLI | 127 / 0 / 0 |
| Full repository build | Failed once at locked node-pty npm TLS download; not retried |
The failing test is McpHttpServerTests.Dispose_DuringInFlightHandler_DoesNotSurfaceObjectDisposedException. It captures process-global unobserved closed-file exceptions from FileStream.ReadAsync / StreamReader.ReadToEndAsyncInternal. The host, test, and suspected process helper are unchanged, but that does not establish independence from the extraction. The identical reduced selector retains the baseline/candidate discrepancy.
Standalone inherited-pipe and pre-canceled helper probes observed zero events on both binaries and did not identify the producer. No unrelated production fix, excluded test, disabled parallelism, or exception suppression was used to manufacture a pass. The candidate-only failure is an unresolved regression risk, not a proven baseline flake.
The separate first Shared failure was an MXC path-grant assertion requiring a direct-current-user permission on task TEMP. Recovery was limited to an authorized additive inheritable ACE on the verified task-owned directory, preserving existing ACLs. The first Tray failure was missing app-local CRT after the full build stopped; recovery used existing runtime/asset-copy targets only after checking the actual default-platform bin\Debug\...\win-x64 output. All failed and recovery invocations remain recorded.
The required full build independently hit the locked node-pty-1.2.0-beta.12.tgz npm TLS failure. Shared, CLI, WinNode CLI, and SetupEngine built; the actual Tray DLL compiled before post-build npm restore failed. Scoped recovery is not a full-build, shipping, native-inference, or MXC execution result.
All 15 retained TRX reports are nonempty; their original and sanitized outcomes/counts were checked. The full Shared suites contain 32 NotExecuted result entries, even though the summary's notExecuted counter is zero; skip totals use actual entries. Raw host paths, settings, credentials, ACL/SIDs, and full runtime traces are not published. Codex autoreview was unavailable and is not claimed.
Reproduction and acceptance path
- Inspect the extraction and retained failures before adopting it. Use the immutable implementation in an isolated worktree, not another proposal's outputs. Resolve the candidate Shared/reduced failure before claiming acceptance.
- Run the standalone
tests\OpenClaw.Mcp.Sdk.Tests\OpenClaw.Mcp.Sdk.Tests.csprojexplicitly; it is not registered in the solution. Account for all 19 methods / 32 cases with zero skips. - Run baseline and candidate Shared selectors with
FullyQualifiedName~McpToolBridgeTests|FullyQualifiedName~McpHttpServerTests|FullyQualifiedName~McpHttpServerTelemetryTests. Retain the reducedFullyQualifiedName~BoundedProcessWaitTests|FullyQualifiedName~McpHttpServerTestsdiscrepancy until explained. - Follow the ledger's exact full-build, Shared, Tray, and WinNode CLI commands and configuration. Restore/build fresh projects before using
--no-buildor--no-restore; a zero exit without a nonempty report is not proof. - Agree on version, ID, notification, cancellation, session, and SSE/plain-JSON behavior before an HTTP migration. Then exercise actual migrated HTTP with a real client and preserve every auth/origin/host/body/deadline/admission/drain invariant.
The passing stream inventory covers real official client/server negotiation and live discovery, arguments and serialized-text results, error sanitization, diagnostic propagation, cancellation/deadline/shutdown, and the three bounded delivery cases. Safe fake denial capabilities prove diagnostic propagation, not Windows consent, execution approvals, or sandbox enforcement.
Migrated HTTP, full 2026 lifecycle, multi-client/early cancellation, real OS consent/MXC, gateway/UI, ARM64, and broader transport races remain unverified. The stream proof establishes a feasible integration boundary; it does not discharge the failed candidate acceptance or production migration requirements.
I can put up a PR if there is interest.
- Dominant language
- C#
- Stars
- 2.1k
- Forks
- 315
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 127
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openclaw/openclaw-windows-node
-
clawsweeper:source-repro impact:ux-release-blocker issue-rating: 🦞 diamond lobster P0
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
openclaw/openclaw-windows-node#1723 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
clawsweeper:needs-maintainer-review clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:security issue-rating: 🦞 diamond lobster P2
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
openclaw/openclaw-windows-node#1516 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
clawsweeper:needs-info clawsweeper:needs-maintainer-review clawsweeper:no-new-fix-pr impact:ux-friction issue-rating: 🦐 gold shrimp P1 stale
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
openclaw/openclaw-windows-node#1145 · 5 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
clawsweeper:needs-security-review clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:security issue-rating: 🦞 diamond lobster P1
Difficulty 3/5 1-2 days Newbie friendliness 35/100
openclaw/openclaw-windows-node#1736 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
clawsweeper:needs-live-repro clawsweeper:needs-maintainer-review clawsweeper:no-new-fix-pr impact:ux-release-blocker issue-rating: 🐚 platinum hermit P0
Difficulty 4/5 3-5 days Newbie friendliness 32/100
openclaw/openclaw-windows-node#1725 · 4 comments · 1 reaction ·
Maintainers usually reply within 1 day
All issues in openclaw/openclaw-windows-node
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Sector-Vestige/Sector-Vestige#476 ·
Maintainers usually reply within 1 day
-
S: Untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Trauma-Station/Trauma-Station#5058 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 2 days
-
Difficulty 1/5 Under an hour Newbie friendliness 75/100
HoffmanEngineering/3d-print-log-api#243 ·
Maintainers usually reply within 1 day
-
:books: documentation untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day