1/**
2 * @internal
3 */
4import * as Effect from "../../../Effect.ts"
5import * as Base64 from "../../../encoding/Base64.ts"
6import * as Match from "../../../Match.ts"
7import * as Schema from "../../../Schema.ts"
8import * as PublicMcpSchema from "../../McpSchema.ts"
9import * as McpCore from "../mcpCore.ts"
10import * as McpProtocol from "../mcpProtocol.ts"
11import * as McpRuntime from "../mcpRuntime.ts"
12import * as McpSchema from "../mcpSchema/v2025_03_26.ts"
13
14// Pings can arrive before initialize and do not require a client session.
15const ClientRequestRpcs = McpSchema.ClientRequestRpcs.omit("ping").middleware(
16 PublicMcpSchema.McpServerClientMiddleware
17).add(McpSchema.Ping)
18
19const ClientRpcs = ClientRequestRpcs.merge(McpSchema.ClientNotificationRpcs)
20
21const AdapterRpcs = ClientRpcs.omit("ping")
22
23const profileFromInitialize = (
24 initialize: typeof McpSchema.Initialize.payloadSchema.Type
25): McpCore.NegotiatedProtocolProfile => ({
26 protocolVersion: McpSchema.protocolVersion,
27 clientCapabilities: PublicMcpSchema.ClientCapabilities.make(initialize.capabilities),
28 clientInfo: PublicMcpSchema.Implementation.make(initialize.clientInfo),
29 requestMetadata: initialize._meta
30})
31
32const unsupported = (
33 operation: PublicMcpSchema.McpReverseOperationUnsupported["operation"],
34 reason: string
35) =>
36 new PublicMcpSchema.McpReverseOperationUnsupported({
37 operation,
38 protocolVersion: McpSchema.protocolVersion,
39 reason
40 })
41
42const requireCapability = (
43 profile: McpCore.NegotiatedProtocolProfile,
44 operation: PublicMcpSchema.McpReverseOperationUnsupported["operation"],
45 capability: "roots" | "sampling"
46) =>
47 Object.hasOwn(profile.clientCapabilities, capability) &&
48 profile.clientCapabilities[capability] !== undefined
49 ? Effect.void
50 : Effect.fail(unsupported(operation, `Client did not advertise the ${capability} capability`))
51
52const projectContent = Effect.fnUntraced(function*(content: PublicMcpSchema.ContentBlock) {
53 const projected = Match.value(content).pipe(
54 Match.when({ type: "text" }, (content) => ({
55 ...content,
56 annotations: content.annotations
57 })),
58 Match.when({ type: Match.is("image", "audio") }, (content) => ({
59 type: content.type,
60 mimeType: content.mimeType,
61 data: Base64.encode(content.data),
62 annotations: content.annotations
63 })),
64 Match.when({ type: "resource" }, (content) => {
65 const resource = content.resource
66 if ("text" in resource) {
67 return McpSchema.EmbeddedResource.make({
68 type: "resource",
69 resource: {
70 uri: resource.uri,
71 mimeType: resource.mimeType,
72 text: resource.text
73 },
74 annotations: content.annotations
75 })
76 }
77 return McpSchema.EmbeddedResource.make({
78 type: "resource",
79 resource: {
80 uri: resource.uri,
81 mimeType: resource.mimeType,
82 blob: Base64.encode(resource.blob)
83 },
84 annotations: content.annotations
85 })
86 }),
87 Match.when({ type: "resource_link" }, () =>
88 new McpCore.UnsupportedByProtocol({
89 protocolVersion: McpSchema.protocolVersion,
90 feature: "resource_link tool content"
91 })),
92 Match.exhaustive
93 )
94 return projected instanceof McpCore.UnsupportedByProtocol
95 ? yield* projected
96 : projected
97})
98
99const projectResourceContents = (
100 content: PublicMcpSchema.TextResourceContents | PublicMcpSchema.BlobResourceContents
101): typeof McpSchema.ResourceContents.Type =>
102 "text" in content
103 ? {
104 uri: content.uri,
105 mimeType: content.mimeType,
106 text: content.text
107 }
108 : {
109 uri: content.uri,
110 mimeType: content.mimeType,
111 blob: Base64.encode(content.blob)
112 }
113
114/**
115 * @internal
116 */
117export const protocol = McpProtocol.make({
118 protocolVersion: McpSchema.protocolVersion,
119 runtime: McpRuntime.stateful({
120 jsonRpc: { acceptsBatches: true },
121 http: { requiresVersionHeader: false }
122 }),
123 clientRpcs: ClientRpcs,
124 clientNotificationRpcs: McpSchema.ClientNotificationRpcs,
125 serverRequestRpcs: McpSchema.ServerRequestRpcs,
126 serverNotificationRpcs: McpSchema.ServerNotificationRpcs,
127 handlerRpcs: AdapterRpcs,
128 makeHandlers: (core, lifecycle) =>
129 AdapterRpcs.of({
130 initialize: (request, { client }) =>
131 lifecycle.initialize(McpSchema.protocolVersion, profileFromInitialize(request), client.id).pipe(
132 Effect.map((result) =>
133 McpSchema.InitializeResult.make({
134 protocolVersion: McpSchema.protocolVersion,
135 capabilities: ({
136 experimental: result.capabilities.experimental,
137 extensions: result.capabilities.extensions,
138 logging: result.capabilities.logging ? {} : undefined,
139 completions: result.capabilities.completions ? {} : undefined,
140 prompts: result.capabilities.prompts,
141 resources: result.capabilities.resources,
142 tools: result.capabilities.tools
143 }),
144 serverInfo: result.serverInfo,
145 instructions: result.instructions
146 })
147 )
148 ),
149 "logging/setLevel": ({ level }, { client, headers }) =>
150 lifecycle.setLogLevel(level, client.id, headers).pipe(Effect.as({})),
151 "notifications/cancelled": () => Effect.void,
152 "notifications/initialized": (_, { client, headers }) =>
153 lifecycle.clientNotification(McpCore.ClientNotification.Initialized(), client.id, headers),
154 "notifications/progress": (progress, { client, headers }) =>
155 lifecycle.clientNotification(
156 McpCore.ClientNotification.Progress({
157 progressToken: progress.progressToken,
158 progress: progress.progress,
159 total: progress.total,
160 message: progress.message,
161 metadata: progress._meta
162 }),
163 client.id,
164 headers
165 ),
166 "notifications/roots/list_changed": (_, { client, headers }) =>
167 lifecycle.clientNotification(McpCore.ClientNotification.RootsChanged(), client.id, headers),
168 "resources/list": (_pageRequest) =>
169 PublicMcpSchema.McpServerClient.use((request) => core.resources.list(McpProtocol.profileFromClient(request)))
170 .pipe(
171 Effect.map((resources) =>
172 McpSchema.ListResourcesResult.make({
173 resources: resources.map((resource) => ({
174 uri: resource.uri,
175 name: resource.name,
176 description: resource.description,
177 mimeType: resource.mimeType,
178 size: resource.size,
179 annotations: resource.annotations
180 }))
181 })
182 )
183 ),
184 "resources/templates/list": (_pageRequest) =>
185 PublicMcpSchema.McpServerClient.use((request) =>
186 core.resources.listTemplates(McpProtocol.profileFromClient(request))
187 ).pipe(
188 Effect.map((resourceTemplates) =>
189 McpSchema.ListResourceTemplatesResult.make({
190 resourceTemplates: resourceTemplates.map((template) => ({
191 uriTemplate: template.uriTemplate,
192 name: template.name,
193 description: template.description,
194 mimeType: template.mimeType,
195 annotations: template.annotations
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(projectResourceContents),
207 _meta: result._meta
208 })
209 }),
210 "resources/subscribe": ({ uri }, { client, headers }) =>
211 lifecycle.subscribe(uri, client.id, headers).pipe(Effect.as({})),
212 "resources/unsubscribe": ({ uri }, { client, headers }) =>
213 lifecycle.unsubscribe(uri, client.id, headers).pipe(Effect.as({})),
214 "prompts/list": (_pageRequest) =>
215 PublicMcpSchema.McpServerClient.use((request) => core.prompts.list(McpProtocol.profileFromClient(request)))
216 .pipe(
217 Effect.map((prompts) =>
218 McpSchema.ListPromptsResult.make({
219 prompts: prompts.map((prompt) => ({
220 name: prompt.name,
221 description: prompt.description,
222 arguments: prompt.arguments?.map((argument) => ({
223 name: argument.name,
224 description: argument.description,
225 required: argument.required
226 }))
227 }))
228 })
229 )
230 ),
231 "prompts/get": Effect.fnUntraced(function*({ arguments: args, name }) {
232 const request = yield* PublicMcpSchema.McpServerClient
233 const outcome = yield* core.prompts.get(name, args ?? {}, McpProtocol.invocationFromClient(request)).pipe(
234 Effect.mapError(McpProtocol.ProtocolError.fromFeature)
235 )
236 if (outcome._tag === "InputRequired") {
237 return yield* McpProtocol.ProtocolError.fromFeature(
238 new McpCore.UnsupportedByProtocol({
239 protocolVersion: McpSchema.protocolVersion,
240 feature: "prompt input requirements"
241 })
242 )
243 }
244 const result = outcome.value
245 const messages = yield* Effect.forEach(result.messages, (message) =>
246 projectContent(message.content).pipe(
247 Effect.map((content) => ({ role: message.role, content })),
248 Effect.mapError(McpProtocol.ProtocolError.fromTool)
249 ))
250 return McpSchema.GetPromptResult.make({
251 description: result.description,
252 messages,
253 _meta: result._meta
254 })
255 }),
256 "completion/complete": Effect.fnUntraced(function*(completeRequest) {
257 const request = yield* PublicMcpSchema.McpServerClient
258 const result = yield* core.completions.complete({
259 reference: completeRequest.ref.type === "ref/prompt"
260 ? { type: "prompt", name: completeRequest.ref.name }
261 : { type: "resourceTemplate", uriTemplate: completeRequest.ref.uri },
262 argument: completeRequest.argument,
263 metadata: completeRequest._meta
264 }, McpProtocol.invocationFromClient(request)).pipe(Effect.mapError(McpProtocol.ProtocolError.fromFeature))
265 return McpSchema.CompleteResult.make({
266 completion: {
267 values: Array.from(result.values),
268 total: result.total,
269 hasMore: result.hasMore
270 },
271 _meta: result.metadata
272 })
273 }),
274 "tools/list": Effect.fnUntraced(function*() {
275 const request = yield* PublicMcpSchema.McpServerClient
276 const tools = yield* core.tools.list(McpProtocol.profileFromClient(request))
277 return McpSchema.ListToolsResult.make({
278 tools: tools.map((tool) =>
279 McpSchema.Tool.make({
280 name: tool.name,
281 description: tool.description,
282 inputSchema: tool.inputSchema,
283 annotations: tool.title === undefined && tool.annotations === undefined
284 ? undefined
285 : McpSchema.ToolAnnotations.make({
286 ...tool.annotations,
287 title: tool.title
288 })
289 })
290 )
291 })
292 }),
293 "tools/call": Effect.fnUntraced(function*(call) {
294 const request = yield* PublicMcpSchema.McpServerClient
295 const result = yield* core.tools.call(
296 { ...call, arguments: call.arguments ?? {} },
297 McpProtocol.invocationFromClient(request)
298 ).pipe(
299 Effect.flatMap((outcome) => McpProtocol.requireCompleteOperation(McpSchema.protocolVersion, outcome)),
300 Effect.mapError(McpProtocol.ProtocolError.fromTool)
301 )
302 const content = yield* Effect.forEach(McpProtocol.unwrapStringStructuredContent(result), projectContent).pipe(
303 Effect.mapError(McpProtocol.ProtocolError.fromTool)
304 )
305 return McpSchema.CallToolResult.make({
306 content,
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: () =>
350 Effect.fail(unsupported("elicitation/create", "Elicitation was introduced after this protocol revision"))
351 }),
352 projectNotification: (notification) =>
353 McpProtocol.makeNotificationProjector({
354 supportsProgressMessage: true
355 }, notification),
356 normalizeCancellation: (payload) =>
357 Schema.decodeUnknownEffect(McpSchema.CancelledNotification.payloadSchema)(payload).pipe(
358 Effect.map((request) => ({
359 requestId: request.requestId,
360 reason: request.reason,
361 metadata: request._meta
362 }))
363 )
364})