Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Proposal: separate MCP protocol maintenance from node execution using the official C# SDK

Open
#1,530 2 comments 1 reaction 0 assignees View on GitHub

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

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

clawsweeper:needs-maintainer-review clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr issue-rating: 🌊 off-meta tidepool P3 stale

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:

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

  1. 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.
  2. Run the standalone tests\OpenClaw.Mcp.Sdk.Tests\OpenClaw.Mcp.Sdk.Tests.csproj explicitly; it is not registered in the solution. Account for all 19 methods / 32 cases with zero skips.
  3. Run baseline and candidate Shared selectors with FullyQualifiedName~McpToolBridgeTests|FullyQualifiedName~McpHttpServerTests|FullyQualifiedName~McpHttpServerTelemetryTests. Retain the reduced FullyQualifiedName~BoundedProcessWaitTests|FullyQualifiedName~McpHttpServerTests discrepancy until explained.
  4. Follow the ledger's exact full-build, Shared, Tray, and WinNode CLI commands and configuration. Restore/build fresh projects before using --no-build or --no-restore; a zero exit without a nonempty report is not proof.
  5. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openclaw/openclaw-windows-node

All issues in openclaw/openclaw-windows-node

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.