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