1import type { NonEmptyReadonlyArray } from "../../Array.ts"
2import * as Cause from "../../Cause.ts"
3import * as Effect from "../../Effect.ts"
4import type * as Rpc from "../../rpc/Rpc.ts"
5import type * as RpcGroup from "../../rpc/RpcGroup.ts"
6import type * as RpcMessage from "../../rpc/RpcMessage.ts"
7import type * as McpProtocol from "../McpProtocol.ts"
8import type * as McpProtocolInternal from "./mcpProtocol.ts"
9
10type AnyRpcGroup = RpcGroup.RpcGroup<any>
11
12const prefix = (protocol: {
13 readonly protocolVersion: string
14}): string => `@effect/mcp/${encodeURIComponent(protocol.protocolVersion)}/`
15
16/**
17 * @internal
18 */
19export interface ProtocolRegistry<
20 Protocol extends McpProtocol.AnyProtocolAdapter = McpProtocol.AnyProtocolAdapter
21> {
22 readonly protocols: NonEmptyReadonlyArray<Protocol>
23 readonly clientRpcs: AnyRpcGroup
24 readonly select: (offeredVersion: string) => Protocol
25 readonly protocolForInternalTag: (tag: string) => Protocol
26 readonly routeClientRequest: (
27 protocol: Protocol,
28 request: RpcMessage.RequestEncoded
29 ) => RpcMessage.RequestEncoded
30 readonly handlerTarget: (
31 contextMap: Map<string, unknown>,
32 context: McpProtocolInternal.HandlerInstallationContext
33 ) => McpProtocolInternal.HandlerInstallationTarget
34}
35
36// NOTE: Protocol selection and request namespacing happen before an adapter's
37// payload codec decodes the request. Canonical McpSchema value reuse must not
38// introduce a shared permissive decode-first path.
39/**
40 * @internal
41 */
42export const make = Effect.fnUntraced(function*<
43 const Protocols extends NonEmptyReadonlyArray<McpProtocol.AnyProtocolAdapter>
44>(
45 protocols: Protocols
46) {
47 type Protocol = Protocols[number]
48
49 if (protocols.length === 0) {
50 return yield* new Cause.IllegalArgumentError(
51 "MCP protocol declaration must contain at least one MCP protocol"
52 )
53 }
54
55 const snapshot = Object.freeze(Array.from(protocols)) as NonEmptyReadonlyArray<Protocol>
56 const byVersion = new Map<string, Protocol>()
57 for (const protocol of snapshot) {
58 if (byVersion.has(protocol.protocolVersion)) {
59 return yield* new Cause.IllegalArgumentError(
60 `Duplicate MCP protocol version: ${protocol.protocolVersion}`
61 )
62 }
63 byVersion.set(protocol.protocolVersion, protocol)
64 }
65 let clientRpcs = snapshot[0].clientRpcs.prefix(prefix(snapshot[0]))
66 for (let i = 1; i < snapshot.length; i++) {
67 clientRpcs = clientRpcs.merge(snapshot[i].clientRpcs.prefix(prefix(snapshot[i])))
68 }
69
70 return {
71 protocols: snapshot,
72 clientRpcs,
73 select: (offeredVersion: string) => byVersion.get(offeredVersion) ?? snapshot[0],
74 protocolForInternalTag: (tag: string) =>
75 snapshot.find((protocol) => tag.startsWith(prefix(protocol))) ?? snapshot[0],
76 routeClientRequest: (
77 protocol: Protocol,
78 request: RpcMessage.RequestEncoded
79 ) => ({
80 ...request,
81 tag: `${prefix(protocol)}${request.tag}`
82 }),
83 handlerTarget: (contextMap, context): McpProtocolInternal.HandlerInstallationTarget => ({
84 context,
85 install: Effect.fnUntraced(function*<
86 Rpcs extends Rpc.Any,
87 Handlers extends RpcGroup.HandlersFrom<Rpcs>
88 >(
89 protocol: {
90 readonly protocolVersion: string
91 },
92 rpcs: RpcGroup.RpcGroup<Rpcs>,
93 handlers: Handlers
94 ) {
95 const handlerContext = yield* rpcs.toHandlers(handlers)
96 for (const rpcDefinition of rpcs.requests.values()) {
97 const namespacedRpc = clientRpcs.requests.get(
98 `${prefix(protocol)}${rpcDefinition._tag}`
99 )
100 const handler = handlerContext.mapUnsafe.get(rpcDefinition.key)
101 if (namespacedRpc === undefined || handler === undefined) {
102 return yield* Effect.die(
103 `MCP handler registration invariant failed for ${protocol.protocolVersion}/${rpcDefinition._tag}`
104 )
105 }
106 contextMap.set(namespacedRpc.key, handler)
107 }
108 })
109 })
110 } satisfies ProtocolRegistry<Protocol>
111})