Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0df1717
Add failing tests for request-scoped McpServer.layerHttp mode
effect-bot Oct 11, 2026
55d4a8e
Add request-scoped mode to McpServer.layerHttp
effect-bot Oct 11, 2026
273dd35
Cover mixed-mode, post-header cancellation, and overlapping request-m…
effect-bot Oct 11, 2026
a9782b3
Route request-mode notifications per request and end aborted web stre…
effect-bot Oct 11, 2026
b246de5
Migrate request-scoped MCP tests to separate HTTP constructor
effect-bot Oct 11, 2026
6ccfc5a
Cover aborting a request whose streamed body is no longer being read
effect-bot Oct 11, 2026
06b63f0
Expose request-scoped MCP HTTP as a separate typed layer
effect-bot Oct 11, 2026
0933b44
Interrupt aborted web stream bodies at the consumer
effect-bot Oct 11, 2026
e9e45f1
Handle request aborts missed during HTTP handler startup
effect-bot Oct 11, 2026
bfdd813
Simplify request-scoped MCP HTTP layer and web stream abort handling
tim-smart Oct 11, 2026
2c7e749
Rename request-scoped MCP HTTP test suite
tim-smart Oct 11, 2026
4125c6a
Share the request notification predicate and tidy request-scoped helpers
tim-smart Oct 11, 2026
faa9b95
Cover request-scoped MCP HTTP on workerd via Miniflare
effect-bot Oct 11, 2026
2b5597d
test(ai): cover late registration in request-scoped MCP
effect-bot Oct 11, 2026
0646d45
Share request-scoped MCP handlers and RPC codecs across requests
effect-bot Oct 11, 2026
c2cdb06
Cover per-server custom codecs and request service precedence
effect-bot Oct 11, 2026
29daf5b
Restrict RPC codec caching to built-in JSON factories
effect-bot Oct 11, 2026
9ef1e57
Move the web handler abort release note to its prerequisite PR
effect-bot Oct 11, 2026
75aa52b
Simplify JSON-only RPC codec sharing and request-scoped MCP options
tim-smart Oct 11, 2026
9875925
Fix Deno typing for subscription tools response
tim-smart Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-http-request-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Add `McpServer.layerHttpRequestScoped` for serverless hosts such as Cloudflare Workers. Each POST is served by an MCP server owned by that request, so no fiber or timer outlives the request. The new layer accepts only sessionless protocol adapters such as `v2026_07_28`, with no subscription or session termination options. `McpServer.layerHttp` retains its long-lived server behavior.
2 changes: 2 additions & 0 deletions packages/effect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@
"ajv": "^8.20.0",
"ajv-draft-04": "^1.0.0",
"ast-types": "^0.16.3",
"esbuild": "^0.28.2",
"immer": "^11.1.21",
"miniflare": "5.20260925.0-alpha",
"protobufjs": "^8.8.0",
"tinybench": "^6.2.1",
"valibot": "^1.5.0"
Expand Down
329 changes: 239 additions & 90 deletions packages/effect/src/ai/McpServer.ts

Large diffs are not rendered by default.

70 changes: 55 additions & 15 deletions packages/effect/src/rpc/RpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,57 @@ export const makeNoSerialization: <Rpcs extends Rpc.Any>(
})
})

// Built-in JSON codecs depend only on the RPC schemas and can be shared.
// Other factories may capture state at construction, so keep them per server.
const isSharedCodecFor = (codecFor: RpcSerialization.CodecFor) => codecFor === RpcSerialization.json.codecFor

interface RpcCodecs {
readonly decode: (u: unknown) => Effect.Effect<any, Schema.SchemaError>
readonly encodeChunk: (
u: NonEmptyReadonlyArray<unknown>
) => Effect.Effect<NonEmptyReadonlyArray<unknown>, Schema.SchemaError>
readonly encodeExit: (u: unknown) => Effect.Effect<ResponseExitEncoded["exit"], Schema.SchemaError>
}

const sharedRpcCodecs = new WeakMap<Rpc.AnyWithProps, RpcCodecs>()

const rpcCodecs = (codecFor: RpcSerialization.CodecFor, rpc: Rpc.AnyWithProps): RpcCodecs => {
const shared = isSharedCodecFor(codecFor)
let codecs = shared ? sharedRpcCodecs.get(rpc) : undefined
if (codecs === undefined) {
const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema)
codecs = {
decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)) as any,
encodeChunk: Schema.encodeUnknownEffect(
codecFor(
Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any)
)
) as any,
encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) as any
}
if (shared) sharedRpcCodecs.set(rpc, codecs)
}
return codecs
}

interface DefectCodecs {
readonly encodeDefectUnsafe: (defect: unknown) => unknown
readonly encodeUnknownRequestExit: (u: unknown) => Effect.Effect<ResponseExitEncoded["exit"], Schema.SchemaError>
}

let sharedDefectCodecs: DefectCodecs | undefined

const makeDefectCodecs = (codecFor: RpcSerialization.CodecFor): DefectCodecs => ({
encodeDefectUnsafe: Schema.encodeSync(codecFor(Schema.Defect())),
// Unknown tags have no RPC schema, so use a defect-only exit schema.
encodeUnknownRequestExit: Schema.encodeUnknownEffect(
codecFor(Schema.Exit(Schema.Never, Schema.Never, Schema.Defect()))
) as any
})

const defectCodecs = (codecFor: RpcSerialization.CodecFor): DefectCodecs =>
isSharedCodecFor(codecFor) ? (sharedDefectCodecs ??= makeDefectCodecs(codecFor)) : makeDefectCodecs(codecFor)

// Merging the handler entry services with the request fiber services costs
// O(services), so the merged context is cached per (entry, fiber context)
// pair. Request fibers on persistent connections keep one context across
Expand Down Expand Up @@ -586,11 +637,7 @@ export const make: <Rpcs extends Rpc.Any>(
supportsSpanPropagation,
supportsTransferables
} = yield* Protocol
const encodeDefectUnsafe = Schema.encodeSync(codecFor(Schema.Defect()))
// Unknown tags have no RPC schema, so use a defect-only exit schema.
const encodeUnknownRequestExit: Schemas["encodeExit"] = Schema.encodeUnknownEffect(
codecFor(Schema.Exit(Schema.Never, Schema.Never, Schema.Defect()))
) as any
const { encodeDefectUnsafe, encodeUnknownRequestExit } = defectCodecs(codecFor)
const services = yield* Effect.context<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>>()
const scope = yield* Scope.make()

Expand Down Expand Up @@ -666,16 +713,9 @@ export const make: <Rpcs extends Rpc.Any>(
let schemas = schemasCache.get(rpc)
if (!schemas) {
const entry = Context.getOrUndefinedUnsafe(services, rpc.key) as Rpc.Handler<Rpcs["_tag"]>
const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema)
schemas = {
tag: rpc._tag,
decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)) as any,
encodeChunk: Schema.encodeUnknownEffect(
codecFor(
Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any)
)
) as any,
encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) as any,
...rpcCodecs(codecFor, rpc),
context: entry.context
}
schemasCache.set(rpc, schemas)
Expand Down Expand Up @@ -1109,7 +1149,7 @@ export const makeProtocolWithHttpEffect: (
RpcSerialization.RpcSerialization
> = Effect.fnUntraced(function*(options = {}) {
const serialization = yield* RpcSerialization.RpcSerialization
const encodeDefectUnsafe = Schema.encodeSync(serialization.codecFor(Schema.Defect()))
const { encodeDefectUnsafe } = defectCodecs(serialization.codecFor)
const includesFraming = serialization.includesFraming
const isBinary = !serialization.contentType.includes("json")

Expand Down Expand Up @@ -1569,7 +1609,7 @@ const makeSocketProtocol: Effect.Effect<
RpcSerialization.RpcSerialization
> = Effect.gen(function*() {
const serialization = yield* RpcSerialization.RpcSerialization
const encodeDefectUnsafe = Schema.encodeSync(serialization.codecFor(Schema.Defect()))
const { encodeDefectUnsafe } = defectCodecs(serialization.codecFor)
const disconnects = yield* Queue.make<number>()

let clientId = 0
Expand Down
Loading
Loading