Skip to content

feat: add v2026-07-28 protocol adapter - #7265

Open
lloydrichards wants to merge 9 commits into
Effect-TS:mainfrom
lloydrichards:feat/v2026-07-28
Open

feat: add v2026-07-28 protocol adapter#7265
lloydrichards wants to merge 9 commits into
Effect-TS:mainfrom
lloydrichards:feat/v2026-07-28

Conversation

@lloydrichards

@lloydrichards lloydrichards commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Type

  • Refactor
  • Feature
  • Bug Fix

Description

Add support the McpProtocol.v2026_07_28 which introduced the 'modern' stateless mcp architecture, while still supporting the previous session-based protocol versions. To support both lifecycles, we extract the protocol lifecycle handling into dedicated runtimes. McpServer selects the appropriate runtime for each adapter: older adapters keep their stateful initialization and session behavior, while v2026_07_28 selects and decodes the protocol from each request without creating session state.

The main user-facing additions are:

  • Stateless server/discover negotiation, with no initialization handshake or session state.
  • McpRequestContext gives handlers access to request and client capabilities neutral to the runtime of the protocol.
  • Multi-round-trip tool results (MRTR) where handlers can return McpSchema.InputRequired, and clients can provide keyed elicitation, sampling, or roots responses on a later request.
  • subscriptions/listen, delivering filtered server change notifications over stdio or Server-Sent Events.
  • Tool output schemas and structuredContent for every JSON value in the 2026-07-28 adapter. Earlier adapters continue to project only the shapes they support.
  • Request-selected streaming RPC HTTP responses and typed server-to-client notifications.
A toy example of the stateless server and JSON tool output:
const Greet = Tool.make("greet", {
  description: "Return a greeting for the supplied name",
  parameters: Schema.Struct({ name: Schema.String }),
  success: Schema.Struct({ greeting: Schema.String })
})

// New in 2026-07-28: a tool output schema may describe any JSON value, not
// only an object. The adapter returns this array as `structuredContent`.
const ProjectFiles = Tool.make("project_files", {
  description: "Return JSON array output and the request-scoped client name",
  parameters: Tool.EmptyParams,
  success: Schema.Array(Schema.String),
  dependencies: [McpSchema.McpRequestContext]
})

const InspectorToolkit = Toolkit.make(Greet, ProjectFiles)

// New in 2026-07-28: the server selects this adapter from each request's
// metadata and headers; there is no initialize handshake or session ID.
const McpHttp = McpServer.layerHttp({
  name: "effect-mcp-2026-07-28-scratchpad",
  version: "1.0.0",
  description: "Modern-only MCP server for Inspector interoperability checks",
  path: "/mcp",
  protocols: [McpProtocol.v2026_07_28]
}).pipe(Layer.provide(HttpRouter.layer))

const Registrations = McpServer.toolkit(InspectorToolkit).pipe(
  Layer.provideMerge(InspectorToolkit.toLayer(InspectorToolkit.of({
    greet: ({ name }) => Effect.succeed({ greeting: `Hello, ${name}!` }),
    project_files: () =>
      McpSchema.McpRequestContext.pipe(
        // Handlers receive client facts from the current request rather than
        // recovering them from the legacy McpServerClient session facade.
        Effect.map((request) => [
          "README.md",
          "package.json",
          `served-for-${request.clientInfo?.name ?? "unknown-client"}.txt`
        ])
      )
  })))
)

const HttpLive = Registrations.pipe(
  Layer.provideMerge(McpHttp),
  Layer.provide(HttpRouter.serve(McpHttp, { disableLogger: true })),
  Layer.provide(NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 3001 }))
)

NodeRuntime.runMain(Layer.launch(HttpLive))

It wasn't possible to model a purely incremental session-era adapter due to the runtime changes, so this PR introduces a few structural changes to both Mcp and Rpc:

  • ProtocolAdapters now declare whether they use a stateful or stateless lifecycle policy.
  • McpServer owns lifecycle runtime state, separating the existing sessionful path from the 2026-07-28 request-scoped path.
  • Dated protocol selection happens from raw request metadata before protocol-specific decoding.
  • Common handlers receive McpRequestContext; legacy reverse operations continue to use McpServerClient.
  • RPC HTTP transport support now has a request-selected streaming response path and typed notification.

Supporting subscriptions/listen required a reusable RPC transport api for streaming responses and typed server-to-client notifications. The RPC change adds on top of the existing buffered HTTP behavior, while MCP can opt into streaming responses and notifications.

How to Review

The files that deserve the most scrutiny should be:

  • mcpProtocol/v2026_07_28.ts + mcpSchema/v2026_07_28.ts -> dated wire behavior
  • mcpRuntime.ts + mcpStatefulRuntime.ts -> stateless and preserved session lifecycles
  • RpcServer.ts -> streaming HTTP responses and notifications

But its probably going to be easiest to just go through each commit ad there is a clear set of packages of work across each:

1. Runtime extraction

  • refactor: introduce MCP lifecycle runtimes and request context

Extracts the lifecycle ownership from the protocol adapters into McpServer runtimes.

This separates the existing sessionful protocol path from the new stateless path without retrofitting sessions onto 2026-07-28. It also introduces McpRequestContext, allowing common handlers to read per-request protocol, capability, and client facts while legacy reverse operations keep using McpServerClient.

2. Adding v2026-07-28

  • feat: add MCP 2026-07-28 protocol adapter

Adds the dated wire schemas and adapter for MCP 2026-07-28.

The adapter is selected from raw HTTP headers and request _meta before dated payload decoding. It serves server/discover, requires self-contained routing metadata, and deliberately creates or consults no Mcp-Session-Id state.

3. Adding MRTR

  • feat: support MCP multi-round-trip tool results

Adds multi-round-trip tool results (MRTR).

Tool handlers can return McpSchema.InputRequired to ask for keyed elicitation, sampling, or roots input. A later self-contained request provides the corresponding responses through McpRequestContext, preserving the stateless model rather than holding a reverse-RPC session open.

4. Adding subscriptions

  • feat: support RPC HTTP streaming and server notifications
  • feat: add MCP 2026-07-28 subscriptions

First adds the transport primitive: request-selected streaming HTTP responses and typed server-to-client notifications for HTTP and stdio.

The protocol layer then uses that primitive for subscriptions/listen, which delivers filtered MCP change notifications over SSE or stdio without changing legacy notification behavior.

5. Clean up and compatibility hardening

  • feat: support JSON-valued MCP tool outputs
  • test: organize MCP conformance suites by protocol behavior
  • fix: make MCP list-change scheduling deterministic

Finishes the user-facing output model and makes version compatibility explicit:

  • 2026-07-28 tools can declare output schemas and return structuredContent for any JSON value; earlier adapters project only the shapes they support.
  • Conformance tests are split into named, behavior-owned suites. Each dated entrypoint selects the suites that apply, rather than embedding revision branches in shared tests.
  • List-change notification scheduling is coalesced deterministically so delayed registration events do not leak into later subscriptions.

WIP

Related

@changeset-bot

changeset-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e2d4c73

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 6.96 KB 6.96 KB 0.00 KB (0.00%)
batching.ts 9.81 KB 9.81 KB 0.00 KB (0.00%)
brand.ts 6.55 KB 6.55 KB 0.00 KB (0.00%)
cache.ts 10.73 KB 10.73 KB 0.00 KB (0.00%)
config.ts 21.32 KB 21.32 KB 0.00 KB (0.00%)
differ.ts 20.27 KB 20.27 KB 0.00 KB (0.00%)
http-client.ts 21.66 KB 21.66 KB 0.00 KB (0.00%)
logger.ts 10.91 KB 10.91 KB 0.00 KB (0.00%)
metric.ts 8.95 KB 8.95 KB 0.00 KB (0.00%)
optic.ts 6.71 KB 6.71 KB 0.00 KB (0.00%)
pubsub.ts 14.99 KB 14.99 KB 0.00 KB (0.00%)
queue.ts 11.67 KB 11.67 KB 0.00 KB (0.00%)
schedule.ts 10.84 KB 10.84 KB 0.00 KB (0.00%)
schema-class.ts 19.90 KB 19.90 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 30.08 KB 30.08 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 26.05 KB 26.05 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.56 KB 13.56 KB 0.00 KB (0.00%)
schema-string.ts 11.06 KB 11.06 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.35 KB 15.35 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 22.00 KB 22.00 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.45 KB 24.45 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 19.23 KB 19.23 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 19.06 KB 19.06 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.92 KB 18.92 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 23.32 KB 23.32 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.54 KB 19.54 KB 0.00 KB (0.00%)
schema.ts 19.13 KB 19.13 KB 0.00 KB (0.00%)
stm.ts 12.72 KB 12.72 KB 0.00 KB (0.00%)
stream.ts 9.76 KB 9.76 KB 0.00 KB (0.00%)

@IMax153 IMax153 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lloydrichards - honestly this looks pretty good to me. The example you provided is quite nice for describing an MCP server.

What is our current test coverage with the 2026-07-28 features? Do we cover the entire spec? Or are we missing pieces currently.

description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
outputSchema: Schema.is(McpSchema.Tool.fields.outputSchema)(tool.outputSchema)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These schema compilers should be extracted to layer / module scope where possible to avoid re-compiling every time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed with refactor: refinements from feedback

@arjunyel

Copy link
Copy Markdown

@lloydrichards you are a legend, thank you so much!

@lloydrichards

Copy link
Copy Markdown
Contributor Author

What is our current test coverage with the 2026-07-28 features? Do we cover the entire spec? Or are we missing pieces currently.

Previously I was using the test+inspector to verify the v2026-07-28 conformance but I did another audit and added a few explicit spec tests that I was missing with some minor fixes:

Test Results:
 pnpm run test packages/effect/test/unstable/ai/McpServer/v2026_07_28.test.ts
$ vitest packages/effect/test/unstable/ai/McpServer/v2026_07_28.test.ts

 DEV  v4.1.10 /Users/lloyd/Documents/GitHub/open_effect

 ✓  effect  test/unstable/ai/McpServer/v2026_07_28.test.ts (117 tests) 242ms
   ✓ Mcp Conformance (2026-07-28) (1)
     ✓ Base Protocol > General fields (1)
       ✓ should preserve additional result metadata fields when decoding a result 1ms
   ✓ Mcp Conformance (2026-07-28) (11)
     ✓ Base Protocol > Stateless messages (11)
       ✓ should preserve string and numeric identifiers when requests succeed 10ms
       ✓ should reject a request when its JSON-RPC version is invalid 8ms
       ✓ should reject a request when its identifier is not a string or integer 2ms
       ✓ should return method not found when the requested method is unknown 2ms
       ✓ should return invalid params when request parameters do not match the method schema 1ms
       ✓ should send no response when an unknown notification is received 2ms
       ✓ should send no response when notification parameters are invalid 2ms
       ✓ should send exactly one result response when a request succeeds 2ms
       ✓ should send exactly one error response when a request fails 4ms
       ✓ should return a parse error when a JSON message is malformed 2ms
       ✓ should return invalid request when a JSON-RPC message omits its method 1ms
   ✓ Mcp Conformance (2026-07-28) (8)
     ✓ Transports > Stateless modern (8)
       ✓ should exchange one compact newline-delimited JSON-RPC message per stdio line 3ms
       ✓ should reconstruct a UTF-8 stdio message when its bytes arrive in separate chunks 2ms
       ✓ should process consecutive stateless stdio requests independently 3ms
       ✓ should shut down the stdio server when the client closes stdin 1ms
       ✓ should require application/json content and both supported response media types for HTTP POST 3ms
       ✓ should return application/json when an HTTP request has one JSON-RPC response 1ms
       ✓ should reject GET and unsupported HTTP methods when only POST is available 0ms
       ✓ should reject every MCP HTTP route when its Origin is not explicitly allowed 0ms
   ✓ Mcp Conformance (2026-07-28) (3)
     ✓ Utilities > Stateless modern (3)
       ✓ Cancellation (3)
         ✓ should send no response when a cancellation notification is received 2ms
         ✓ should interrupt work and suppress its response when an active request is cancelled 6ms
         ✓ should allow a later request to reuse an identifier cancelled before it was active 2ms
   ✓ Mcp Conformance (2026-07-28) (20)
     ✓ Tools (20)
       ✓ Capabilities (2)
         ✓ MUST advertise the tools capability when tools are registered 6ms
         ✓ MUST NOT advertise the tools capability when tools are not supported 2ms
       ✓ Listing Tools (3)
         ✓ SCHEMA preserves tool names and descriptions 2ms
         ✓ MUST return each tool input schema 2ms
         ✓ should list shared tools and gate modern-only tools by era 2ms
       ✓ Calling Tools (15)
         ✓ MUST call a registered tool with valid arguments 5ms
         ✓ MUST reject an unknown tool name with a protocol error 2ms
         ✓ MUST not invoke a tool handler when argument validation fails 2ms
         ✓ SCHEMA returns text content 7ms
         ✓ SCHEMA returns image content 1ms
         ✓ SCHEMA returns embedded resources 1ms
         ✓ MUST return multiple content items in order 1ms
         ✓ MUST return tool execution failures with isError 1ms
         ✓ MUST keep tool execution errors distinct from protocol errors 1ms
         ✓ SHOULD not expose defects or internal error details 1ms
         ✓ should return base64 audio content when an audio tool is called 1ms
         ✓ Structured content (3)
           ✓ should advertise an object output schema when a tool declares one 2ms
           ✓ should return a resource link when a resource-link tool is called 1ms
           ✓ should return structured content when a structured tool is called 1ms
         ✓ should report invalid tool arguments according to the selected revision 1ms
   ✓ Mcp Conformance (2026-07-28) (5)
     ✓ Tools > JSON Schema 2020-12 (3)
       ✓ should preserve 2020-12 keywords when listing an input schema 4ms
       ✓ should list a non-object output schema when the tool declares one 2ms
       ✓ should return primitive structured content when declared by the tool 2ms
     ✓ Tools > Modern request headers (1)
       ✓ should reject a tools/call request when its required routing name is missing 1ms
     ✓ Tools > Modern listing (1)
       ✓ should return tools in the same order when the registered inventory has not changed 2ms
   ✓ Mcp Conformance (2026-07-28) (12)
     ✓ Resources (12)
       ✓ Capabilities (2)
         ✓ MUST advertise resources when resources are registered 3ms
         ✓ MUST NOT advertise resources when resources are not supported 1ms
       ✓ Listing Resources (2)
         ✓ MUST list every resource visible to the initialized client 2ms
         ✓ SCHEMA preserves resource URI, name, description, and MIME type 1ms
       ✓ Reading Resources (5)
         ✓ MUST read text resource contents 1ms
         ✓ MUST read binary resource contents as base64 1ms
         ✓ SCHEMA preserves the resource URI and MIME type in returned contents 1ms
         ✓ MUST return multiple resource contents in order 1ms
         ✓ should return the revision-specific error when the resource URI is unknown 1ms
       ✓ Resource Templates (3)
         ✓ MUST list every registered resource template 2ms
         ✓ MUST match and decode a concrete resource-template URI 1ms
         ✓ MUST not invoke the handler when template parameter decoding fails 1ms
   ✓ Mcp Conformance (2026-07-28) (16)
     ✓ Prompts (16)
       ✓ Capabilities (2)
         ✓ MUST advertise prompts when prompts are registered 4ms
         ✓ MUST NOT advertise prompts when prompts are not supported 1ms
       ✓ Listing Prompts (3)
         ✓ MUST list every prompt visible to the initialized client 2ms
         ✓ SCHEMA preserves prompt names, descriptions, and arguments 1ms
         ✓ MUST mark required and optional prompt arguments correctly 1ms
       ✓ Getting Prompts (11)
         ✓ MUST get a registered prompt without arguments 2ms
         ✓ MUST get a registered prompt with valid arguments 1ms
         ✓ SHOULD reject an unknown prompt name with Invalid Params 1ms
         ✓ SHOULD reject missing required prompt arguments with Invalid Params 1ms
         ✓ SHOULD reject prompt arguments with invalid values 1ms
         ✓ MUST not invoke the prompt handler when argument validation fails 1ms
         ✓ SCHEMA preserves the prompt description and message order 1ms
         ✓ MUST return text message content 1ms
         ✓ MUST return image message content 1ms
         ✓ MUST return embedded resource message content 1ms
         ✓ should return base64 audio content when an audio prompt is requested 1ms
   ✓ Mcp Conformance (2026-07-28) (9)
     ✓ Completion (9)
       ✓ Capabilities (1)
         ✓ MUST advertise completions when argument completion is supported 3ms
       ✓ Requesting Completions (8)
         ✓ MUST complete a prompt argument 1ms
         ✓ MUST complete a resource template argument 1ms
         ✓ SHOULD reject an unknown prompt reference with Invalid Params 1ms
         ✓ MUST reject an unknown argument name 1ms
         ✓ MUST return completion values in order 1ms
         ✓ SCHEMA returns the total and additional-results indicator 1ms
         ✓ MUST return at most one hundred completion values 1ms
         ✓ should pass previously resolved arguments when completion context is supplied 1ms
   ✓ Mcp Conformance (2026-07-28) (4)
     ✓ Logging > Stateless modern (4)
       ✓ should advertise logging when request-scoped log filtering is supported 2ms
       ✓ should apply every specified log level to the request that declares it 10ms
       ✓ should reject a request when its request-scoped log level is unknown 1ms
       ✓ should preserve the level, logger, and JSON data when decoding a log notification 0ms
   ✓ Mcp Conformance (2026-07-28) (3)
     ✓ Multi round-trip requests (3)
       ✓ should return supported keyed input requests and resume when matching responses are supplied 5ms
       ✓ should remain input-required when continuation keys or request state do not match 2ms
       ✓ should reject a continuation when its input responses or request state are malformed 1ms
   ✓ Mcp Conformance (2026-07-28) (12)
     ✓ Subscriptions (12)
       ✓ should advertise supported subscription capabilities when features are registered 2ms
       ✓ should not advertise subscriptions and should reject listen when the transport cannot send notifications 2ms
       ✓ should acknowledge with the exact identifier when it is numeric or string-valued 4ms
       ✓ should deliver a change notification when its kind is requested 3ms
       ✓ should deliver a resource update when its URI is subscribed 3ms
       ✓ should acknowledge only the supported subset when requested filters exceed server capabilities 3ms
       ✓ should deliver a matching event to each subscription 3ms
       ✓ should keep another subscription active when its peer is cancelled 3ms
       ✓ should preserve notification metadata and own the subscription identifier 2ms
       ✓ should deliver modern changes only when an active subscription matches 4ms
       ✓ should preserve legacy notification delivery when modern and legacy adapters are configured 3ms
       ✓ should stream acknowledgment before matching events when using HTTP 3ms
   ✓ Mcp Conformance (2026-07-28) (13)
     ✓ Lifecycle (2)
       ✓ should discover the server when no initialization or session exists 2ms
       ✓ should serve independent requests when no discovery or session exists 1ms
     ✓ Transports (5)
       ✓ should exchange self-contained newline-delimited requests over stdio 1ms
       ✓ should accept every required routing name when the header matches the request 2ms
       ✓ should reject routing headers when they are missing, malformed, or mismatched 1ms
       ✓ should reject an unsupported request version with the supported versions 0ms
       ✓ should return method not found when a request uses an unknown or removed method 1ms
     ✓ Request metadata (3)
       ✓ should reject requests when required protocol metadata is missing 0ms
       ✓ should accept a request when optional client identity is omitted 0ms
       ✓ should preserve caller metadata alongside authoritative protocol facts 2ms
     ✓ Result envelopes (2)
       ✓ should attach modern result and cache metadata to every cacheable operation 5ms
       ✓ should attach a complete result type and server identity to every non-cacheable operation 2ms
     ✓ Multi round-trip request capabilities (1)
       ✓ should reject input requests when the client omits their required capabilities 1ms

 Test Files  1 passed (1)
      Tests  117 passed (117)
   Start at  08:16:54
   Duration  1.65s (transform 1.02s, setup 737ms, import 563ms, tests 242ms, environment 0ms)

Its now a pretty solid 117 test coverage that I'm feeling confident is conformant with the new spec.

I would probably do another pass on the Mcp Conformance suite afterwards though to clean up how the .statelessModernSuite() and.statefulLegacySuite() are organized since its a bit hard judge what the overlap is with the compound changelog of the spec.

But with this, I'm gonna say the PR is ready for review 🤞

@lloydrichards
lloydrichards marked this pull request as ready for review August 17, 2026 07:36
@lloydrichards
lloydrichards requested a review from IMax153 August 17, 2026 07:37
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpProtocol/v2026_07_28.ts Outdated
Comment thread packages/effect/src/unstable/ai/internal/mcpSchema/v2026_07_28.ts
@lloydrichards

lloydrichards commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

I'm not sure about the rpc changes. I think there might a simpler way of doing it.

Another option I explored was adding another variant to the RpcMessage.FromServerEncoded:

interface ServerNotificationEncoded {
  readonly _tag: "ServerNotification"
  readonly tag: string
  readonly payload: unknown
}

type FromServerEncoded =
  | ResponseChunkEncoded
  | ResponseExitEncoded
  | ResponseDefectEncoded
  | ServerNotificationEncoded
  | Pong
  | ClientProtocolError

and then on the McpServer side of things it could just send like normally:

const protocol = yield* RpcServer.Protocol

// before
yield* protocol.sendNotification(clientId, {
  tag: notification.tag,
  payload
})

// after
yield* protocol.send(clientId, {
  _tag: "ServerNotification",
  tag: notification.tag,
  payload
})

This was simpler from the API perspective but had the side-effect of needing to add the same functionality to the RpcClient for the decoding. Once I saw that I was going to need to touch the client as well I stopped and backtracked since it felt like the scope was blowing up a bit.

If this direction feels better or if you want to add this functionality to the Rpc separately from this PR then I can always wait for you to have a think and implement a version you'd be happier with.

@lloydrichards

Copy link
Copy Markdown
Contributor Author

I spied the #7354 and rebased everything against the first-class support for Rpc server notification (thank you Tim 🙏). I've moved the SSE logic into McpServer and tidied up all the refinements into a single commit.

let me know if you think anything else needs changing

@ebramanti

Copy link
Copy Markdown
Contributor

Eager to see this land, great work @lloydrichards 🙂

Comment thread .changeset/calm-pandas-listen.md Outdated
@@ -0,0 +1,5 @@
---
"effect": patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should only be one changeset for this PR.

})
const projectStructuredContent = (
content: Schema.Json | undefined
): Schema.JsonObject | undefined => content === undefined || Schema.is(Schema.JsonObject)(content) ? content : undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could use something cheaper than Schema.is here. Maybe something from Predicate.

Also the isToolOutputSchema is essentially doing the same thing. Schema.is(Schema.JsonObject) will walk the entire structure / tree ensure each field is json encodable.

I think we could probably remove the use of Schema.Json everywhere and use something less strict.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll replace Schema.is(Schema.JsonObject) with Predicate.isReadonlyObject. For isToolOutputSchema, I still need a few shallow property checks because the v2025-06-18 and v2025-11-25 schemas require an object-rooted outputSchema with object-valued properties and a string-valued required array.

2026-07-28 loosened these restrictions to support full JSON Schema 2020-12 output schemas. That broader contract is already implemented by the mcpSchema/v2026_07_28 adapter. The legacy checks are shallow compatibility checks, avoiding the full recursive validation performed by Schema.is.

}, 0)
if (!pendingListChanged.has(message.tag)) {
pendingListChanged.add(message.tag)
yield* Effect.sleep(0).pipe(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this sleep? Forking already defers execution and doesn't run it immediately.

Also I'm not seeing any real reason to refactor this to a generator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here the original issue i had was the setTimeout leaking across my test suit to i originally just wanted to replace it with Effect.sleep to control the TestClock. The later addition of the Effect.forkDetach though does double this so i'll simplify it 👍

onFromClient: (options) =>
Effect.suspend((): Effect.Effect<void> => {
onFromClient: (options): Effect.Effect<void> =>
Effect.gen(function*() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect.fnUntraced

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried the Effect.fnUntraced here but it created circular inference from the handler referenced notifications.write while notifications was being constructed. I can broke that cycle:

let writeNotification!: (
  message: ServerNotificationResponse
) => Effect.Effect<void>

const notifications = yield* RpcClient.makeNoSerialization(
  BroadcastServerNotificationRpcs,
  {
    onFromClient: Effect.fnUntraced(function*(options) {
      // ...
      yield* writeNotification({
        clientId: 0,
        requestId: message.id,
        _tag: "Exit",
        exit: Exit.void
      })
    })
  }
)

writeNotification = notifications.write

But this felt like an ugly solution when the Effect.gen worked fine.

clientId: number,
notification: McpProtocol.ProjectedNotification
) =>
Effect.gen(function*() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a Effect.suspend

Comment on lines +975 to +978
Effect.scoped,
Effect.ignoreCause,
Effect.forkIn(serverScope),
Effect.asVoid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Effect.scoped,
Effect.ignoreCause,
Effect.forkIn(serverScope),
Effect.asVoid
Effect.scoped,
Effect.forkIn(serverScope),
Effect.asVoid

Also the roots aren't used, so do we need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove it as it was a bit of legacy work around but the spec doesn't require this behaviour.

https://modelcontextprotocol.io/specification/2025-06-18/client/roots

)
}),
Effect.catch((error) =>
request.isNotification

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isNotification path seems to be quite different compared to a normal request. It might be worth splitting them.

@lloydrichards lloydrichards Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I separated MCP notification-only methods from normal requests 👍

I also had a look at splitting the full isNotification path, but that behavior feels like it belongs in RpcServer, which can run them normally while suppressing the response. Implementing it locally in the McpServer would add unsafe state or bypassed normal RPC handling, so I kept this PR to the small, safe fix.

@lloydrichards

Copy link
Copy Markdown
Contributor Author

@tim-smart I've made most of the changes or commented with reasons. Let me know if there are still blocking issues. I'm gonna be away from tomorrow for a week (🏖️) so if there is anything then it'll need to wait until i'm back. thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support MCP protocol version 2026-07-28 (v4)

5 participants