1import * as Effect from "../../../Effect.ts"
2import * as Base64 from "../../../encoding/Base64.ts"
3import * as Match from "../../../Match.ts"
4import * as Predicate from "../../../Predicate.ts"
5import * as Schema from "../../../Schema.ts"
6import * as PublicMcpSchema from "../../McpSchema.ts"
7import * as McpCore from "../mcpCore.ts"
8import * as McpProtocol from "../mcpProtocol.ts"
9import * as McpRuntime from "../mcpRuntime.ts"
10import * as McpSchema from "../mcpSchema/v2025_11_25.ts"
11
12// Pings can arrive before initialize and do not require a client session.
13const ClientRequestRpcs = McpSchema.ClientRequestRpcs.omit("ping").middleware(
14 PublicMcpSchema.McpServerClientMiddleware
15).add(McpSchema.Ping)
16
17const ClientRpcs = ClientRequestRpcs.merge(McpSchema.ClientNotificationRpcs)
18
19const AdapterRpcs = ClientRpcs.omit("ping")
20const isToolOutputSchema = (
21 schema: PublicMcpSchema.ToolOutputJson | undefined
22): schema is PublicMcpSchema.ToolJson => {
23 if (schema?.type !== "object") {
24 return false
25 }
26
27 const isPropertiesSupported = schema.properties === undefined ||
28 (Predicate.isReadonlyObject(schema.properties) &&
29 Object.values(schema.properties).every(Predicate.isReadonlyObject))
30 const isRequiredDefined = schema.required === undefined ||
31 (Array.isArray(schema.required) && schema.required.every(Predicate.isString))
32
33 return isPropertiesSupported && isRequiredDefined
34}
35
36const unsupported = (
37 operation: PublicMcpSchema.McpReverseOperationUnsupported["operation"],
38 reason: string
39) =>
40 new PublicMcpSchema.McpReverseOperationUnsupported({
41 operation,
42 protocolVersion: McpSchema.protocolVersion,
43 reason
44 })
45
46const requireCapability = (
47 profile: McpCore.NegotiatedProtocolProfile,
48 operation: PublicMcpSchema.McpReverseOperationUnsupported["operation"],
49 capability: "roots" | "sampling" | "elicitation"
50) =>
51 Object.hasOwn(profile.clientCapabilities, capability) &&
52 profile.clientCapabilities[capability] !== undefined
53 ? Effect.void
54 : Effect.fail(unsupported(operation, `Client did not advertise the ${capability} capability`))
55
56const resultRequiresSamplingTools = (result: typeof McpSchema.CreateMessage.successSchema.Type): boolean =>
57 result.stopReason === "toolUse" ||
58 ("type" in result.content
59 ? result.content.type === "tool_use" || result.content.type === "tool_result"
60 : result.content.some((block) => block.type === "tool_use" || block.type === "tool_result"))
61
62const hasElicitationModeCapability = (
63 profile: McpCore.NegotiatedProtocolProfile,
64 mode: "form" | "url"
65): boolean => {
66 const elicitation = profile.clientCapabilities.elicitation
67 if (elicitation === undefined) return false
68 return mode === "form"
69 ? elicitation.form !== undefined || elicitation.url === undefined
70 : elicitation.url !== undefined
71}
72
73const projectContent = Effect.fnUntraced(function*(content: PublicMcpSchema.ContentBlock) {
74 return Match.value(content).pipe(
75 Match.when({ type: Match.is("text", "resource_link") }, (content) => content),
76 Match.when({ type: Match.is("image", "audio") }, (content) => ({
77 type: content.type,
78 mimeType: content.mimeType,
79 data: Base64.encode(content.data),
80 annotations: content.annotations,
81 _meta: content._meta
82 })),
83 Match.when({ type: "resource" }, (content) => {
84 const resource = content.resource
85 if ("text" in resource) {
86 return McpSchema.EmbeddedResource.make({
87 type: "resource",
88 resource: {
89 uri: resource.uri,
90 mimeType: resource.mimeType,
91 _meta: resource._meta,
92 text: resource.text
93 },
94 annotations: content.annotations,
95 _meta: content._meta
96 })
97 }
98 return McpSchema.EmbeddedResource.make({
99 type: "resource",
100 resource: {
101 uri: resource.uri,
102 mimeType: resource.mimeType,
103 _meta: resource._meta,
104 blob: Base64.encode(resource.blob)
105 },
106 annotations: content.annotations,
107 _meta: content._meta
108 })
109 }),
110 Match.exhaustive
111 )
112})
113
114const projectStructuredContent = (
115 content: Schema.Json | undefined
116): Schema.JsonObject | undefined => content === undefined || isJsonObject(content) ? content : undefined
117
118const isJsonObject = (value: Schema.Json): value is Schema.JsonObject => Predicate.isReadonlyObject(value)
119
120/**
121 * @internal
122 */
123export const protocol = McpProtocol.make({
124 protocolVersion: McpSchema.protocolVersion,
125 runtime: McpRuntime.stateful({
126 jsonRpc: { acceptsBatches: false },
127 http: { requiresVersionHeader: true }
128 }),
129 clientRpcs: ClientRpcs,
130 clientNotificationRpcs: McpSchema.ClientNotificationRpcs,
131 serverRequestRpcs: McpSchema.ServerRequestRpcs,
132 serverNotificationRpcs: McpSchema.ServerNotificationRpcs,
133 handlerRpcs: AdapterRpcs,
134 makeHandlers: (core, lifecycle) =>
135 AdapterRpcs.of({
136 initialize: (request, { client }) =>
137 lifecycle.initialize(McpSchema.protocolVersion, {
138 protocolVersion: McpSchema.protocolVersion,
139 clientCapabilities: PublicMcpSchema.ClientCapabilities.make(request.capabilities),
140 clientInfo: PublicMcpSchema.Implementation.make(request.clientInfo),
141 requestMetadata: request._meta
142 }, client.id).pipe(
143 Effect.map((result) =>
144 McpSchema.InitializeResult.make({
145 protocolVersion: McpSchema.protocolVersion,
146 capabilities: ({
147 experimental: result.capabilities.experimental,
148 extensions: result.capabilities.extensions,
149 logging: result.capabilities.logging ? {} : undefined,
150 completions: result.capabilities.completions ? {} : undefined,
151 prompts: result.capabilities.prompts,
152 resources: result.capabilities.resources,
153 tools: result.capabilities.tools
154 }),
155 serverInfo: result.serverInfo,
156 instructions: result.instructions
157 })
158 )
159 ),
160 "logging/setLevel": ({ level }, { client, headers }) =>
161 lifecycle.setLogLevel(level, client.id, headers).pipe(Effect.as({})),
162 "notifications/cancelled": () => Effect.void,
163 "notifications/initialized": (_, { client, headers }) =>
164 lifecycle.clientNotification(McpCore.ClientNotification.Initialized(), client.id, headers),
165 "notifications/progress": (progress, { client, headers }) =>
166 lifecycle.clientNotification(
167 McpCore.ClientNotification.Progress({
168 progressToken: progress.progressToken,
169 progress: progress.progress,
170 total: progress.total,
171 message: progress.message,
172 metadata: progress._meta
173 }),
174 client.id,
175 headers
176 ),
177 "notifications/roots/list_changed": (_, { client, headers }) =>
178 lifecycle.clientNotification(McpCore.ClientNotification.RootsChanged(), client.id, headers),
179 "resources/list": () =>
180 PublicMcpSchema.McpServerClient.use((request) => core.resources.list(McpProtocol.profileFromClient(request)))
181 .pipe(
182 Effect.map((resources) =>
183 McpSchema.ListResourcesResult.make({
184 resources: resources.map((resource) => McpSchema.Resource.make(resource))
185 })
186 )
187 ),
188 "resources/templates/list": () =>
189 PublicMcpSchema.McpServerClient.use((request) =>
190 core.resources.listTemplates(McpProtocol.profileFromClient(request))
191 ).pipe(
192 Effect.map((resourceTemplates) =>
193 McpSchema.ListResourceTemplatesResult.make({
194 resourceTemplates: resourceTemplates.map((resourceTemplate) =>
195 McpSchema.ResourceTemplate.make(resourceTemplate)
196 )
197 })
198 )
199 ),
200 "resources/read": Effect.fnUntraced(function*({ uri }) {
201 const request = yield* PublicMcpSchema.McpServerClient
202 const result = yield* core.resources.read(uri, McpProtocol.invocationFromClient(request)).pipe(
203 Effect.mapError(McpProtocol.ProtocolError.fromFeature)
204 )
205 return McpSchema.ReadResourceResult.make({
206 contents: result.contents.map((content) =>
207 "text" in content
208 ? {
209 uri: content.uri,
210 mimeType: content.mimeType,
211 _meta: content._meta,
212 text: content.text
213 }
214 : {
215 uri: content.uri,
216 mimeType: content.mimeType,
217 _meta: content._meta,
218 blob: Base64.encode(content.blob)
219 }
220 ),
221 _meta: result._meta
222 })
223 }),
224 "resources/subscribe": ({ uri }, { client, headers }) =>
225 lifecycle.subscribe(uri, client.id, headers).pipe(Effect.as({})),
226 "resources/unsubscribe": ({ uri }, { client, headers }) =>
227 lifecycle.unsubscribe(uri, client.id, headers).pipe(Effect.as({})),
228 "prompts/list": () =>
229 PublicMcpSchema.McpServerClient.use((request) => core.prompts.list(McpProtocol.profileFromClient(request)))
230 .pipe(
231 Effect.map((prompts) =>
232 McpSchema.ListPromptsResult.make({
233 prompts: prompts.map((prompt) => McpSchema.Prompt.make(prompt))
234 })
235 )
236 ),
237 "prompts/get": Effect.fnUntraced(function*({ arguments: args, name }) {
238 const request = yield* PublicMcpSchema.McpServerClient
239 const outcome = yield* core.prompts.get(name, args ?? {}, McpProtocol.invocationFromClient(request)).pipe(
240 Effect.mapError(McpProtocol.ProtocolError.fromFeature)
241 )
242 if (outcome._tag === "InputRequired") {
243 return yield* McpProtocol.ProtocolError.fromFeature(
244 new McpCore.UnsupportedByProtocol({
245 protocolVersion: McpSchema.protocolVersion,
246 feature: "prompt input requirements"
247 })
248 )
249 }
250 const result = outcome.value
251 const messages = yield* Effect.forEach(result.messages, (message) =>
252 projectContent(message.content).pipe(
253 Effect.map((content) => ({ role: message.role, content })),
254 Effect.mapError(McpProtocol.ProtocolError.fromTool)
255 ))
256 return McpSchema.GetPromptResult.make({
257 description: result.description,
258 messages,
259 _meta: result._meta
260 })
261 }),
262 "completion/complete": Effect.fnUntraced(function*(completeRequest) {
263 const request = yield* PublicMcpSchema.McpServerClient
264 const result = yield* core.completions.complete({
265 reference: completeRequest.ref.type === "ref/prompt"
266 ? {
267 type: "prompt",
268 name: completeRequest.ref.name,
269 title: completeRequest.ref.title
270 }
271 : { type: "resourceTemplate", uriTemplate: completeRequest.ref.uri },
272 argument: completeRequest.argument,
273 context: completeRequest.context?.arguments === undefined
274 ? undefined
275 : { arguments: completeRequest.context.arguments },
276 metadata: completeRequest._meta
277 }, McpProtocol.invocationFromClient(request)).pipe(Effect.mapError(McpProtocol.ProtocolError.fromFeature))
278 return McpSchema.CompleteResult.make({
279 completion: {
280 values: Array.from(result.values),
281 total: result.total,
282 hasMore: result.hasMore
283 },
284 _meta: result.metadata
285 })
286 }),
287 "tools/list": Effect.fnUntraced(function*() {
288 const request = yield* PublicMcpSchema.McpServerClient
289 const tools = yield* core.tools.list(McpProtocol.profileFromClient(request))
290 return McpSchema.ListToolsResult.make({
291 tools: tools.map((tool) =>
292 McpSchema.Tool.make({
293 name: tool.name,
294 title: tool.title,
295 description: tool.description,
296 inputSchema: tool.inputSchema,
297 outputSchema: isToolOutputSchema(tool.outputSchema)
298 ? tool.outputSchema
299 : undefined,
300 icons: tool.icons,
301 annotations: tool.annotations === undefined
302 ? undefined
303 : McpSchema.ToolAnnotations.make({
304 title: tool.annotations.title,
305 readOnlyHint: tool.annotations.readOnlyHint,
306 destructiveHint: tool.annotations.destructiveHint,
307 idempotentHint: tool.annotations.idempotentHint,
308 openWorldHint: tool.annotations.openWorldHint
309 }),
310 _meta: tool._meta
311 })
312 )
313 })
314 }),
315 "tools/call": Effect.fnUntraced(function*(call) {
316 const request = yield* PublicMcpSchema.McpServerClient
317 const result = yield* core.tools.call(
318 { ...call, arguments: call.arguments ?? {} },
319 McpProtocol.invocationFromClient(request)
320 ).pipe(
321 Effect.flatMap((outcome) => McpProtocol.requireCompleteOperation(McpSchema.protocolVersion, outcome)),
322 Effect.catchTag(["InvalidToolInput", "ToolExecutionError"], (error) =>
323 Effect.succeed(PublicMcpSchema.CallToolResult.make({
324 content: [PublicMcpSchema.TextContent.make({
325 type: "text",
326 text: error.message
327 })],
328 isError: true
329 }))),
330 Effect.mapError(McpProtocol.ProtocolError.fromTool)
331 )
332 const content = yield* Effect.forEach(McpProtocol.unwrapStringStructuredContent(result), projectContent).pipe(
333 Effect.mapError(McpProtocol.ProtocolError.fromTool)
334 )
335 const structuredContent = projectStructuredContent(result.structuredContent)
336 return McpSchema.CallToolResult.make({
337 content,
338 structuredContent,
339 isError: result.isError,
340 _meta: result._meta
341 })
342 })
343 }),
344 toReverseClient: (profile, client) => ({
345 listRoots: Effect.fnUntraced(function*(request) {
346 yield* requireCapability(profile, "roots/list", "roots")
347 const wireRequest = yield* McpProtocol.transcode(
348 PublicMcpSchema.ListRoots.payloadSchema,
349 McpSchema.ListRoots.payloadSchema,
350 request
351 ).pipe(
352 Effect.mapError(() => unsupported("roots/list", "Request is not representable by this protocol"))
353 )
354 const { roots } = yield* client["roots/list"](wireRequest).pipe(
355 Effect.mapError(McpProtocol.reverseError("roots/list"))
356 )
357 return new PublicMcpSchema.ListRootsResult({ roots })
358 }),
359 createMessage: Effect.fnUntraced(function*(request) {
360 yield* requireCapability(profile, "sampling/createMessage", "sampling")
361 if (
362 McpProtocol.samplingRequestRequiresTools(request) && profile.clientCapabilities.sampling?.tools == undefined
363 ) {
364 return yield* unsupported("sampling/createMessage", "Client did not advertise the sampling.tools capability")
365 }
366 if (
367 (request.includeContext === "thisServer" || request.includeContext === "allServers") &&
368 profile.clientCapabilities.sampling?.context == undefined
369 ) {
370 return yield* unsupported("sampling/createMessage", "Client did not advertise the sampling.context capability")
371 }
372 const wireRequest = yield* McpProtocol.transcode(
373 PublicMcpSchema.CreateMessage.payloadSchema,
374 McpSchema.CreateMessage.payloadSchema,
375 request
376 ).pipe(
377 Effect.mapError(() => unsupported("sampling/createMessage", "Request is not representable by this protocol"))
378 )
379 const result = yield* client["sampling/createMessage"](wireRequest).pipe(
380 Effect.mapError(McpProtocol.reverseError("sampling/createMessage"))
381 )
382 if (resultRequiresSamplingTools(result) && profile.clientCapabilities.sampling?.tools == undefined) {
383 return yield* unsupported("sampling/createMessage", "Client did not advertise the sampling.tools capability")
384 }
385 return yield* McpProtocol.transcode(
386 McpSchema.CreateMessage.successSchema,
387 PublicMcpSchema.CreateMessage.successSchema,
388 result
389 ).pipe(
390 Effect.mapError(() =>
391 unsupported("sampling/createMessage", "Response is not representable by the canonical model")
392 )
393 )
394 }),
395 elicit: Effect.fnUntraced(function*(request) {
396 yield* requireCapability(profile, "elicitation/create", "elicitation")
397 const mode = request.mode === "url" ? "url" : "form"
398 if (!hasElicitationModeCapability(profile, mode)) {
399 return yield* unsupported("elicitation/create", `Client did not advertise the elicitation.${mode} capability`)
400 }
401 const wireRequest = yield* McpProtocol.transcode(
402 PublicMcpSchema.Elicit.payloadSchema,
403 McpSchema.Elicit.payloadSchema,
404 request
405 ).pipe(
406 Effect.mapError(() => unsupported("elicitation/create", "Request is not representable by this protocol"))
407 )
408 const result = yield* client["elicitation/create"](wireRequest).pipe(
409 Effect.mapError(McpProtocol.reverseError("elicitation/create"))
410 )
411 return yield* McpProtocol.transcode(
412 McpSchema.Elicit.successSchema,
413 PublicMcpSchema.Elicit.successSchema,
414 result
415 ).pipe(
416 Effect.mapError(() => unsupported("elicitation/create", "Response is not representable by the canonical model"))
417 )
418 })
419 }),
420 projectNotification: (notification) =>
421 notification._tag === "ElicitationComplete"
422 ? Effect.succeed({
423 tag: "notifications/elicitation/complete",
424 payload: { elicitationId: notification.elicitationId }
425 })
426 : McpProtocol.makeNotificationProjector({
427 supportsProgressMessage: true
428 }, notification),
429 normalizeCancellation: (payload) =>
430 Schema.decodeUnknownEffect(McpSchema.CancelledNotification.payloadSchema)(payload).pipe(
431 Effect.map((request) => ({
432 requestId: request.requestId,
433 reason: request.reason,
434 metadata: request._meta
435 }))
436 )
437})