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/v2024_11_05.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 projectCapabilities = (
53 capabilities: McpCore.CanonicalServerCapabilities
54): typeof McpSchema.ServerCapabilities.Type => ({
55 experimental: capabilities.experimental,
56 extensions: capabilities.extensions,
57 logging: capabilities.logging ? {} : undefined,
58 prompts: capabilities.prompts,
59 resources: capabilities.resources,
60 tools: capabilities.tools
61})
62
63const projectContent = Effect.fnUntraced(function*(content: PublicMcpSchema.ContentBlock) {
64 const projected = Match.value(content).pipe(
65 Match.when({ type: "text" }, (content) => ({
66 ...content,
67 annotations: content.annotations
68 })),
69 Match.when({ type: "image" }, (content) =>
70 McpSchema.ImageContent.make({
71 type: "image",
72 mimeType: content.mimeType,
73 data: Base64.encode(content.data),
74 annotations: content.annotations
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 text: resource.text
85 },
86 annotations: content.annotations
87 })
88 }
89 return McpSchema.EmbeddedResource.make({
90 type: "resource",
91 resource: {
92 uri: resource.uri,
93 mimeType: resource.mimeType,
94 blob: Base64.encode(resource.blob)
95 },
96 annotations: content.annotations
97 })
98 }),
99 Match.when({ type: Match.is("audio", "resource_link") }, (content) =>
100 new McpCore.UnsupportedByProtocol({
101 protocolVersion: McpSchema.protocolVersion,
102 feature: `${content.type} tool content`
103 })),
104 Match.exhaustive
105 )
106 return projected instanceof McpCore.UnsupportedByProtocol
107 ? yield* projected
108 : projected
109})
110
111/**
112 * @internal
113 */
114export const protocol = McpProtocol.make({
115 protocolVersion: McpSchema.protocolVersion,
116 runtime: McpRuntime.stateful({
117 jsonRpc: { acceptsBatches: false },
118 http: { requiresVersionHeader: false }
119 }),
120 clientRpcs: ClientRpcs,
121 clientNotificationRpcs: McpSchema.ClientNotificationRpcs,
122 serverRequestRpcs: McpSchema.ServerRequestRpcs,
123 serverNotificationRpcs: McpSchema.ServerNotificationRpcs,
124 handlerRpcs: AdapterRpcs,
125 makeHandlers: (core, lifecycle) =>
126 AdapterRpcs.of({
127 initialize: (request, { client }) =>
128 lifecycle.initialize(McpSchema.protocolVersion, profileFromInitialize(request), client.id).pipe(
129 Effect.map((result) =>
130 McpSchema.InitializeResult.make({
131 protocolVersion: McpSchema.protocolVersion,
132 capabilities: projectCapabilities(result.capabilities),
133 serverInfo: result.serverInfo,
134 instructions: result.instructions
135 })
136 )
137 ),
138 "logging/setLevel": ({ level }, { client, headers }) =>
139 lifecycle.setLogLevel(level, client.id, headers).pipe(Effect.as({})),
140 "notifications/cancelled": () => Effect.void,
141 "notifications/initialized": (_, { client, headers }) =>
142 lifecycle.clientNotification(McpCore.ClientNotification.Initialized(), client.id, headers),
143 "notifications/progress": (progress, { client, headers }) =>
144 lifecycle.clientNotification(
145 McpCore.ClientNotification.Progress({
146 progressToken: progress.progressToken,
147 progress: progress.progress,
148 total: progress.total,
149 metadata: progress._meta
150 }),
151 client.id,
152 headers
153 ),
154 "notifications/roots/list_changed": (_, { client, headers }) =>
155 lifecycle.clientNotification(McpCore.ClientNotification.RootsChanged(), client.id, headers),
156 "resources/list": (_pageRequest) =>
157 PublicMcpSchema.McpServerClient.use((request) => core.resources.list(McpProtocol.profileFromClient(request)))
158 .pipe(Effect.map((resources) => McpSchema.ListResourcesResult.make({ resources }))),
159 "resources/templates/list": (_pageRequest) =>
160 PublicMcpSchema.McpServerClient.use((request) =>
161 core.resources.listTemplates(McpProtocol.profileFromClient(request))
162 ).pipe(
163 Effect.map((resourceTemplates) => McpSchema.ListResourceTemplatesResult.make({ resourceTemplates }))
164 ),
165 "resources/read": Effect.fnUntraced(function*({ uri }) {
166 const request = yield* PublicMcpSchema.McpServerClient
167 const result = yield* core.resources.read(uri, McpProtocol.invocationFromClient(request)).pipe(
168 Effect.mapError(McpProtocol.ProtocolError.fromFeature)
169 )
170 return McpSchema.ReadResourceResult.make({
171 contents: result.contents.map((content) =>
172 "text" in content
173 ? {
174 uri: content.uri,
175 mimeType: content.mimeType,
176 text: content.text
177 }
178 : {
179 uri: content.uri,
180 mimeType: content.mimeType,
181 blob: Base64.encode(content.blob)
182 }
183 ),
184 _meta: result._meta
185 })
186 }),
187 "resources/subscribe": ({ uri }, { client, headers }) =>
188 lifecycle.subscribe(uri, client.id, headers).pipe(Effect.as({})),
189 "resources/unsubscribe": ({ uri }, { client, headers }) =>
190 lifecycle.unsubscribe(uri, client.id, headers).pipe(Effect.as({})),
191 "prompts/list": () =>
192 PublicMcpSchema.McpServerClient.use((request) => core.prompts.list(McpProtocol.profileFromClient(request)))
193 .pipe(
194 Effect.map((prompts) =>
195 McpSchema.ListPromptsResult.make({
196 prompts: prompts.map((prompt) => ({
197 name: prompt.name,
198 description: prompt.description,
199 arguments: prompt.arguments?.map((argument) => ({
200 name: argument.name,
201 description: argument.description,
202 required: argument.required
203 }))
204 }))
205 })
206 )
207 ),
208 "prompts/get": Effect.fnUntraced(function*({ arguments: args, name }) {
209 const request = yield* PublicMcpSchema.McpServerClient
210 const outcome = yield* core.prompts.get(name, args ?? {}, McpProtocol.invocationFromClient(request)).pipe(
211 Effect.mapError(McpProtocol.ProtocolError.fromFeature)
212 )
213 if (outcome._tag === "InputRequired") {
214 return yield* McpProtocol.ProtocolError.fromFeature(
215 new McpCore.UnsupportedByProtocol({
216 protocolVersion: McpSchema.protocolVersion,
217 feature: "prompt input requirements"
218 })
219 )
220 }
221 const result = outcome.value
222 const messages = yield* Effect.forEach(result.messages, (message) =>
223 projectContent(message.content).pipe(
224 Effect.map((content) => ({ role: message.role, content })),
225 Effect.mapError(McpProtocol.ProtocolError.fromTool)
226 ))
227 return McpSchema.GetPromptResult.make({
228 description: result.description,
229 messages,
230 _meta: result._meta
231 })
232 }),
233 "completion/complete": Effect.fnUntraced(function*(completeRequest) {
234 const request = yield* PublicMcpSchema.McpServerClient
235 const result = yield* core.completions.complete({
236 reference: completeRequest.ref.type === "ref/prompt"
237 ? { type: "prompt", name: completeRequest.ref.name }
238 : { type: "resourceTemplate", uriTemplate: completeRequest.ref.uri },
239 argument: completeRequest.argument,
240 metadata: completeRequest._meta
241 }, McpProtocol.invocationFromClient(request)).pipe(Effect.mapError(McpProtocol.ProtocolError.fromFeature))
242 return McpSchema.CompleteResult.make({
243 completion: {
244 values: Array.from(result.values),
245 total: result.total,
246 hasMore: result.hasMore
247 },
248 _meta: result.metadata
249 })
250 }),
251 "tools/list": Effect.fnUntraced(function*() {
252 const request = yield* PublicMcpSchema.McpServerClient
253 return yield* core.tools.list(McpProtocol.profileFromClient(request)).pipe(
254 Effect.map((tools) =>
255 McpSchema.ListToolsResult.make({
256 tools: tools.map((tool) =>
257 McpSchema.Tool.make({
258 name: tool.name,
259 description: tool.description,
260 inputSchema: tool.inputSchema
261 })
262 )
263 })
264 )
265 )
266 }),
267 "tools/call": Effect.fnUntraced(function*(call) {
268 const request = yield* PublicMcpSchema.McpServerClient
269 const result = yield* core.tools.call(
270 { ...call, arguments: call.arguments ?? {} },
271 McpProtocol.invocationFromClient(request)
272 ).pipe(
273 Effect.flatMap((outcome) => McpProtocol.requireCompleteOperation(McpSchema.protocolVersion, outcome)),
274 Effect.mapError(McpProtocol.ProtocolError.fromTool)
275 )
276 const content = yield* Effect.forEach(McpProtocol.unwrapStringStructuredContent(result), projectContent).pipe(
277 Effect.mapError(McpProtocol.ProtocolError.fromTool)
278 )
279 return McpSchema.CallToolResult.make({
280 content,
281 isError: result.isError,
282 _meta: result._meta
283 })
284 })
285 }),
286 toReverseClient: (profile, client) => ({
287 listRoots: Effect.fnUntraced(function*(request) {
288 yield* requireCapability(profile, "roots/list", "roots")
289 const wireRequest = yield* McpProtocol.transcode(
290 PublicMcpSchema.ListRoots.payloadSchema,
291 McpSchema.ListRoots.payloadSchema,
292 request
293 ).pipe(
294 Effect.mapError(() => unsupported("roots/list", "Request is not representable by this protocol"))
295 )
296 const { roots } = yield* client["roots/list"](wireRequest).pipe(
297 Effect.mapError(McpProtocol.reverseError("roots/list"))
298 )
299 return new PublicMcpSchema.ListRootsResult({ roots })
300 }),
301 createMessage: Effect.fnUntraced(function*(request) {
302 yield* requireCapability(profile, "sampling/createMessage", "sampling")
303 const wireRequest = yield* McpProtocol.transcodeStrict(
304 PublicMcpSchema.CreateMessage.payloadSchema,
305 McpSchema.CreateMessage.payloadSchema,
306 request
307 ).pipe(
308 Effect.mapError(() => unsupported("sampling/createMessage", "Request is not representable by this protocol"))
309 )
310 const result = yield* client["sampling/createMessage"](wireRequest).pipe(
311 Effect.mapError(McpProtocol.reverseError("sampling/createMessage"))
312 )
313 return yield* McpProtocol.transcode(
314 McpSchema.CreateMessage.successSchema,
315 PublicMcpSchema.CreateMessage.successSchema,
316 result
317 ).pipe(
318 Effect.mapError(() =>
319 unsupported("sampling/createMessage", "Response is not representable by the canonical model")
320 )
321 )
322 }),
323 elicit: () =>
324 Effect.fail(unsupported("elicitation/create", "Elicitation was introduced after this protocol revision"))
325 }),
326 projectNotification: (notification) =>
327 McpProtocol.makeNotificationProjector({
328 supportsProgressMessage: false
329 }, notification),
330 normalizeCancellation: (payload) =>
331 Schema.decodeUnknownEffect(McpSchema.CancelledNotification.payloadSchema)(payload).pipe(
332 Effect.map((request) => ({
333 requestId: request.requestId,
334 reason: request.reason,
335 metadata: request._meta
336 }))
337 )
338})