@effect/ai 0.18.12 → 0.18.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1088 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as Headers from "@effect/platform/Headers"
5
+ import type * as HttpRouter from "@effect/platform/HttpRouter"
6
+ import type { RpcMessage } from "@effect/rpc"
7
+ import type * as Rpc from "@effect/rpc/Rpc"
8
+ import * as RpcClient from "@effect/rpc/RpcClient"
9
+ import type * as RpcGroup from "@effect/rpc/RpcGroup"
10
+ import * as RpcSerialization from "@effect/rpc/RpcSerialization"
11
+ import * as RpcServer from "@effect/rpc/RpcServer"
12
+ import * as Arr from "effect/Array"
13
+ import * as Cause from "effect/Cause"
14
+ import * as Context from "effect/Context"
15
+ import * as Effect from "effect/Effect"
16
+ import * as Exit from "effect/Exit"
17
+ import * as JsonSchema from "effect/JSONSchema"
18
+ import * as Layer from "effect/Layer"
19
+ import * as Logger from "effect/Logger"
20
+ import * as Mailbox from "effect/Mailbox"
21
+ import * as Option from "effect/Option"
22
+ import * as Schema from "effect/Schema"
23
+ import * as AST from "effect/SchemaAST"
24
+ import type { Sink } from "effect/Sink"
25
+ import type { Stream } from "effect/Stream"
26
+ import type * as Types from "effect/Types"
27
+ import * as FindMyWay from "find-my-way-ts"
28
+ import * as AiTool from "./AiTool.js"
29
+ import type * as AiToolkit from "./AiToolkit.js"
30
+ import type { CallTool, Complete, GetPrompt, Param, ServerCapabilities } from "./McpSchema.js"
31
+ import {
32
+ Annotations,
33
+ BlobResourceContents,
34
+ CallToolResult,
35
+ ClientRpcs,
36
+ CompleteResult,
37
+ GetPromptResult,
38
+ InternalError,
39
+ InvalidParams,
40
+ ListPromptsResult,
41
+ ListResourcesResult,
42
+ ListResourceTemplatesResult,
43
+ ListToolsResult,
44
+ ParamAnnotation,
45
+ Prompt,
46
+ PromptArgument,
47
+ PromptMessage,
48
+ ReadResourceResult,
49
+ Resource,
50
+ ResourceTemplate,
51
+ ServerNotificationRpcs,
52
+ TextContent,
53
+ TextResourceContents,
54
+ Tool,
55
+ ToolAnnotations
56
+ } from "./McpSchema.js"
57
+
58
+ /**
59
+ * @since 1.0.0
60
+ * @category McpServer
61
+ */
62
+ export class McpServer extends Context.Tag("@effect/ai/McpServer")<
63
+ McpServer,
64
+ {
65
+ readonly notifications: RpcClient.RpcClient<RpcGroup.Rpcs<typeof ServerNotificationRpcs>>
66
+ readonly notificationsMailbox: Mailbox.ReadonlyMailbox<RpcMessage.Request<any>>
67
+ readonly tools: ReadonlyArray<Tool>
68
+ readonly addTool: (options: {
69
+ readonly tool: Tool
70
+ readonly handle: (payload: any) => Effect.Effect<CallToolResult>
71
+ }) => Effect.Effect<void>
72
+ readonly callTool: (
73
+ requests: typeof CallTool.payloadSchema.Type
74
+ ) => Effect.Effect<CallToolResult, InternalError | InvalidParams>
75
+ readonly resources: ReadonlyArray<Resource>
76
+ readonly addResource: (
77
+ resource: Resource,
78
+ handle: Effect.Effect<ReadResourceResult, InternalError>
79
+ ) => Effect.Effect<void>
80
+ readonly resourceTemplates: ReadonlyArray<ResourceTemplate>
81
+ readonly addResourceTemplate: (
82
+ options: {
83
+ readonly template: ResourceTemplate
84
+ readonly routerPath: string
85
+ readonly completions: Record<string, (input: string) => Effect.Effect<CompleteResult, InternalError>>
86
+ readonly handle: (
87
+ uri: string,
88
+ params: Array<string>
89
+ ) => Effect.Effect<ReadResourceResult, InvalidParams | InternalError>
90
+ }
91
+ ) => Effect.Effect<void>
92
+ readonly findResource: (uri: string) => Effect.Effect<ReadResourceResult, InvalidParams | InternalError>
93
+ readonly addPrompt: (options: {
94
+ readonly prompt: Prompt
95
+ readonly completions: Record<string, (input: string) => Effect.Effect<CompleteResult, InternalError>>
96
+ readonly handle: (params: Record<string, string>) => Effect.Effect<GetPromptResult, InternalError | InvalidParams>
97
+ }) => Effect.Effect<void>
98
+ readonly prompts: ReadonlyArray<Prompt>
99
+ readonly getPromptResult: (
100
+ request: typeof GetPrompt.payloadSchema.Type
101
+ ) => Effect.Effect<GetPromptResult, InternalError | InvalidParams>
102
+ readonly completion: (complete: typeof Complete.payloadSchema.Type) => Effect.Effect<CompleteResult, InternalError>
103
+ }
104
+ >() {
105
+ /**
106
+ * @since 1.0.0
107
+ */
108
+ static readonly make = Effect.gen(function*() {
109
+ const matcher = makeUriMatcher<
110
+ {
111
+ readonly _tag: "ResourceTemplate"
112
+ readonly handle: (
113
+ uri: string,
114
+ params: Array<string>
115
+ ) => Effect.Effect<ReadResourceResult, InternalError | InvalidParams>
116
+ } | {
117
+ readonly _tag: "Resource"
118
+ readonly effect: Effect.Effect<ReadResourceResult, InternalError>
119
+ }
120
+ >()
121
+ const tools = Arr.empty<Tool>()
122
+ const toolMap = new Map<string, (payload: any) => Effect.Effect<CallToolResult, InternalError>>()
123
+ const resources: Array<Resource> = []
124
+ const resourceTemplates: Array<ResourceTemplate> = []
125
+ const prompts: Array<Prompt> = []
126
+ const promptMap = new Map<
127
+ string,
128
+ (params: Record<string, string>) => Effect.Effect<GetPromptResult, InternalError | InvalidParams>
129
+ >()
130
+ const completionsMap = new Map<string, (input: string) => Effect.Effect<CompleteResult, InternalError>>()
131
+ const notificationsMailbox = yield* Mailbox.make<RpcMessage.Request<any>>()
132
+ const listChangedHandles = new Map<string, any>()
133
+ const notifications = yield* RpcClient.makeNoSerialization(ServerNotificationRpcs, {
134
+ onFromClient(options): Effect.Effect<void> {
135
+ const message = options.message
136
+ if (message._tag !== "Request") {
137
+ return Effect.void
138
+ }
139
+ if (message.tag.includes("list_changed")) {
140
+ if (!listChangedHandles.has(message.tag)) {
141
+ listChangedHandles.set(
142
+ message.tag,
143
+ setTimeout(() => {
144
+ notificationsMailbox.unsafeOffer(message)
145
+ listChangedHandles.delete(message.tag)
146
+ }, 0)
147
+ )
148
+ }
149
+ } else {
150
+ notificationsMailbox.unsafeOffer(message)
151
+ }
152
+ return notifications.write({
153
+ clientId: 0,
154
+ requestId: message.id,
155
+ _tag: "Exit",
156
+ exit: Exit.void as any
157
+ })
158
+ }
159
+ })
160
+
161
+ return McpServer.of({
162
+ notifications: notifications.client,
163
+ notificationsMailbox,
164
+ get tools() {
165
+ return tools
166
+ },
167
+ addTool: (options) =>
168
+ Effect.suspend(() => {
169
+ tools.push(options.tool)
170
+ toolMap.set(options.tool.name, options.handle)
171
+ return notifications.client["notifications/tools/list_changed"]({})
172
+ }),
173
+ callTool: (request) =>
174
+ Effect.suspend((): Effect.Effect<CallToolResult, InternalError | InvalidParams> => {
175
+ const handle = toolMap.get(request.name)
176
+ if (!handle) {
177
+ return Effect.fail(new InvalidParams({ message: `Tool '${request.name}' not found` }))
178
+ }
179
+ return handle(request.arguments)
180
+ }),
181
+ get resources() {
182
+ return resources
183
+ },
184
+ get resourceTemplates() {
185
+ return resourceTemplates
186
+ },
187
+ addResource: (resource, effect) =>
188
+ Effect.suspend(() => {
189
+ resources.push(resource)
190
+ matcher.add(resource.uri, { _tag: "Resource", effect })
191
+ return notifications.client["notifications/resources/list_changed"]({})
192
+ }),
193
+ addResourceTemplate: ({ completions, handle, routerPath, template }) =>
194
+ Effect.suspend(() => {
195
+ resourceTemplates.push(template)
196
+ matcher.add(routerPath, { _tag: "ResourceTemplate", handle })
197
+ for (const [param, handle] of Object.entries(completions)) {
198
+ completionsMap.set(`ref/resource/${template.uriTemplate}/${param}`, handle)
199
+ }
200
+ return notifications.client["notifications/resources/list_changed"]({})
201
+ }),
202
+ findResource: (uri) =>
203
+ Effect.suspend(() => {
204
+ const match = matcher.find(uri)
205
+ if (!match) {
206
+ return Effect.succeed(
207
+ new ReadResourceResult({
208
+ contents: []
209
+ })
210
+ )
211
+ } else if (match.handler._tag === "Resource") {
212
+ return match.handler.effect
213
+ }
214
+ const params: Array<string> = []
215
+ for (const key of Object.keys(match.params)) {
216
+ params[Number(key)] = match.params[key]!
217
+ }
218
+ return match.handler.handle(uri, params)
219
+ }),
220
+ get prompts() {
221
+ return prompts
222
+ },
223
+ addPrompt: (options) =>
224
+ Effect.suspend(() => {
225
+ prompts.push(options.prompt)
226
+ promptMap.set(options.prompt.name, options.handle)
227
+ for (const [param, handle] of Object.entries(options.completions)) {
228
+ completionsMap.set(`ref/prompt/${options.prompt.name}/${param}`, handle)
229
+ }
230
+ return notifications.client["notifications/prompts/list_changed"]({})
231
+ }),
232
+ getPromptResult: Effect.fnUntraced(function*({ arguments: params, name }) {
233
+ const handler = promptMap.get(name)
234
+ if (!handler) {
235
+ return yield* new InvalidParams({ message: `Prompt '${name}' not found` })
236
+ }
237
+ return yield* handler(params ?? {})
238
+ }),
239
+ completion: Effect.fnUntraced(function*(complete) {
240
+ const ref = complete.ref
241
+ const key = ref.type === "ref/resource"
242
+ ? `ref/resource/${ref.uri}/${complete.argument.name}`
243
+ : `ref/prompt/${ref.name}/${complete.argument.name}`
244
+ const handler = completionsMap.get(key)
245
+ return handler ? yield* handler(complete.argument.value) : CompleteResult.empty
246
+ })
247
+ })
248
+ })
249
+
250
+ /**
251
+ * @since 1.0.0
252
+ */
253
+ static readonly layer: Layer.Layer<McpServer> = Layer.scoped(McpServer, McpServer.make)
254
+ }
255
+
256
+ const LATEST_PROTOCOL_VERSION = "2025-03-26"
257
+ const SUPPORTED_PROTOCOL_VERSIONS = [
258
+ LATEST_PROTOCOL_VERSION,
259
+ "2024-11-05",
260
+ "2024-10-07"
261
+ ]
262
+
263
+ /**
264
+ * @since 1.0.0
265
+ * @category Constructors
266
+ */
267
+ export const run = Effect.fnUntraced(function*(options: {
268
+ readonly name: string
269
+ readonly version: string
270
+ }) {
271
+ const protocol = yield* RpcServer.Protocol
272
+ const handlers = yield* Layer.build(layerHandlers(options))
273
+ const server = yield* McpServer
274
+
275
+ const patchedProtocol = RpcServer.Protocol.of({
276
+ ...protocol,
277
+ run: (f) =>
278
+ protocol.run((clientId, request) => {
279
+ if (request._tag === "Request" && request.tag.startsWith("notifications/")) {
280
+ if (request.tag === "notifications/cancelled") {
281
+ return f(clientId, {
282
+ _tag: "Interrupt",
283
+ requestId: String((request.payload as any).requestId)
284
+ })
285
+ }
286
+ const handler = handlers.unsafeMap.get(request.tag) as Rpc.Handler<string>
287
+ return handler
288
+ ? handler.handler(request.payload, Headers.fromInput(request.headers)) as Effect.Effect<void>
289
+ : Effect.void
290
+ }
291
+ return f(clientId, request)
292
+ })
293
+ })
294
+
295
+ const encodeNotification = Schema.encode(
296
+ Schema.Union(...Array.from(ServerNotificationRpcs.requests.values(), (rpc) => rpc.payloadSchema))
297
+ )
298
+ yield* server.notificationsMailbox.take.pipe(
299
+ Effect.flatMap(Effect.fnUntraced(function*(request) {
300
+ const encoded = yield* encodeNotification(request.payload)
301
+ const message: RpcMessage.RequestEncoded = {
302
+ _tag: "Request",
303
+ tag: request.tag,
304
+ payload: encoded
305
+ } as any
306
+ const clientIds = yield* patchedProtocol.clientIds
307
+ for (const clientId of clientIds) {
308
+ yield* patchedProtocol.send(clientId, message as any)
309
+ }
310
+ })),
311
+ Effect.catchAllCause(() => Effect.void),
312
+ Effect.forever,
313
+ Effect.forkScoped
314
+ )
315
+
316
+ return yield* RpcServer.make(ClientRpcs, {
317
+ spanPrefix: "McpServer",
318
+ disableFatalDefects: true
319
+ }).pipe(
320
+ Effect.provideService(RpcServer.Protocol, patchedProtocol),
321
+ Effect.provide(handlers)
322
+ )
323
+ }, Effect.scoped)
324
+
325
+ /**
326
+ * @since 1.0.0
327
+ * @category Layers
328
+ */
329
+ export const layer = (options: {
330
+ readonly name: string
331
+ readonly version: string
332
+ }): Layer.Layer<McpServer, never, RpcServer.Protocol> =>
333
+ Layer.scopedDiscard(Effect.forkScoped(run(options))).pipe(
334
+ Layer.provideMerge(McpServer.layer)
335
+ )
336
+
337
+ /**
338
+ * Run the McpServer, using stdio for input and output.
339
+ *
340
+ * ```ts
341
+ * import { McpSchema, McpServer } from "@effect/ai"
342
+ * import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node"
343
+ * import { Effect, Layer, Logger, Schema } from "effect"
344
+ *
345
+ * const idParam = McpSchema.param("id", Schema.NumberFromString)
346
+ *
347
+ * // Define a resource template for a README file
348
+ * const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({
349
+ * name: "README Template",
350
+ * // You can add auto-completion for the ID parameter
351
+ * completion: {
352
+ * id: (_) => Effect.succeed([1, 2, 3, 4, 5])
353
+ * },
354
+ * content: Effect.fn(function*(_uri, id) {
355
+ * return `# MCP Server Demo - ID: ${id}`
356
+ * })
357
+ * })
358
+ *
359
+ * // Define a test prompt with parameters
360
+ * const TestPrompt = McpServer.prompt({
361
+ * name: "Test Prompt",
362
+ * description: "A test prompt to demonstrate MCP server capabilities",
363
+ * parameters: Schema.Struct({
364
+ * flightNumber: Schema.String
365
+ * }),
366
+ * completion: {
367
+ * flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"])
368
+ * },
369
+ * content: ({ flightNumber }) => Effect.succeed(`Get the booking details for flight number: ${flightNumber}`)
370
+ * })
371
+ *
372
+ * // Merge all the resources and prompts into a single server layer
373
+ * const ServerLayer = Layer.mergeAll(
374
+ * ReadmeTemplate,
375
+ * TestPrompt
376
+ * ).pipe(
377
+ * // Provide the MCP server implementation
378
+ * Layer.provide(McpServer.layerStdio({
379
+ * name: "Demo Server",
380
+ * version: "1.0.0",
381
+ * stdin: NodeStream.stdin,
382
+ * stdout: NodeSink.stdout
383
+ * })),
384
+ * // add a stderr logger
385
+ * Layer.provide(Logger.add(Logger.prettyLogger({ stderr: true })))
386
+ * )
387
+ *
388
+ * Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)
389
+ * ```
390
+ *
391
+ * @since 1.0.0
392
+ * @category Layers
393
+ */
394
+ export const layerStdio = <EIn, RIn, EOut, ROut>(options: {
395
+ readonly name: string
396
+ readonly version: string
397
+ readonly stdin: Stream<Uint8Array, EIn, RIn>
398
+ readonly stdout: Sink<unknown, Uint8Array | string, unknown, EOut, ROut>
399
+ }): Layer.Layer<McpServer, never, RIn | ROut> =>
400
+ layer(options).pipe(
401
+ Layer.provide(RpcServer.layerProtocolStdio({
402
+ stdin: options.stdin,
403
+ stdout: options.stdout
404
+ })),
405
+ Layer.provide(RpcSerialization.layerNdJsonRpc()),
406
+ // remove stdout loggers
407
+ Layer.provideMerge(Logger.remove(Logger.defaultLogger)),
408
+ Layer.provideMerge(Logger.remove(Logger.prettyLoggerDefault))
409
+ )
410
+
411
+ /**
412
+ * Run the McpServer, using HTTP for input and output.
413
+ *
414
+ * ```ts
415
+ * import { McpSchema, McpServer } from "@effect/ai"
416
+ * import { HttpRouter } from "@effect/platform"
417
+ * import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
418
+ * import { Effect, Layer, Schema } from "effect"
419
+ * import { createServer } from "node:http"
420
+ *
421
+ * const idParam = McpSchema.param("id", Schema.NumberFromString)
422
+ *
423
+ * // Define a resource template for a README file
424
+ * const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({
425
+ * name: "README Template",
426
+ * // You can add auto-completion for the ID parameter
427
+ * completion: {
428
+ * id: (_) => Effect.succeed([1, 2, 3, 4, 5])
429
+ * },
430
+ * content: Effect.fn(function*(_uri, id) {
431
+ * return `# MCP Server Demo - ID: ${id}`
432
+ * })
433
+ * })
434
+ *
435
+ * // Define a test prompt with parameters
436
+ * const TestPrompt = McpServer.prompt({
437
+ * name: "Test Prompt",
438
+ * description: "A test prompt to demonstrate MCP server capabilities",
439
+ * parameters: Schema.Struct({
440
+ * flightNumber: Schema.String
441
+ * }),
442
+ * completion: {
443
+ * flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"])
444
+ * },
445
+ * content: ({ flightNumber }) => Effect.succeed(`Get the booking details for flight number: ${flightNumber}`)
446
+ * })
447
+ *
448
+ * // Merge all the resources and prompts into a single server layer
449
+ * const ServerLayer = Layer.mergeAll(
450
+ * ReadmeTemplate,
451
+ * TestPrompt,
452
+ * HttpRouter.Default.serve()
453
+ * ).pipe(
454
+ * // Provide the MCP server implementation
455
+ * Layer.provide(McpServer.layerHttp({
456
+ * name: "Demo Server",
457
+ * version: "1.0.0",
458
+ * path: "/mcp"
459
+ * })),
460
+ * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
461
+ * )
462
+ *
463
+ * Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)
464
+ * ```
465
+ *
466
+ * @since 1.0.0
467
+ * @category Layers
468
+ */
469
+ export const layerHttp = <I = HttpRouter.Default>(options: {
470
+ readonly name: string
471
+ readonly version: string
472
+ readonly path: HttpRouter.PathInput
473
+ readonly routerTag?: HttpRouter.HttpRouter.TagClass<I, string, any, any>
474
+ }): Layer.Layer<McpServer> =>
475
+ layer(options).pipe(
476
+ Layer.provide(RpcServer.layerProtocolHttp(options)),
477
+ Layer.provide(RpcSerialization.layerJsonRpc())
478
+ )
479
+
480
+ /**
481
+ * Register an AiToolkit with the McpServer.
482
+ *
483
+ * @since 1.0.0
484
+ * @category Tools
485
+ */
486
+ export const registerToolkit: <Tools extends AiTool.Any>(toolkit: AiToolkit.AiToolkit<Tools>) => Effect.Effect<
487
+ void,
488
+ never,
489
+ McpServer | AiTool.ToHandler<Tools>
490
+ > = Effect.fnUntraced(function*<Tools extends AiTool.Any>(
491
+ toolkit: AiToolkit.AiToolkit<Tools>
492
+ ) {
493
+ const registry = yield* McpServer
494
+ const built = yield* toolkit
495
+ const context = yield* Effect.context<AiTool.Context<Tools>>()
496
+ for (const tool of built.tools) {
497
+ const mcpTool = new Tool({
498
+ name: tool.name,
499
+ description: tool.description,
500
+ inputSchema: makeJsonSchema(tool.parametersSchema.ast),
501
+ annotations: new ToolAnnotations({
502
+ ...(Context.getOption(tool.annotations, AiTool.Title).pipe(
503
+ Option.map((title) => ({ title })),
504
+ Option.getOrUndefined
505
+ )),
506
+ readOnlyHint: Context.get(tool.annotations, AiTool.Readonly),
507
+ destructiveHint: Context.get(tool.annotations, AiTool.Destructive),
508
+ idempotentHint: Context.get(tool.annotations, AiTool.Idempotent),
509
+ openWorldHint: Context.get(tool.annotations, AiTool.OpenWorld)
510
+ })
511
+ })
512
+ yield* registry.addTool({
513
+ tool: mcpTool,
514
+ handle(payload) {
515
+ return built.handle(tool.name as any, payload).pipe(
516
+ Effect.provide(context),
517
+ Effect.match({
518
+ onFailure: (error) =>
519
+ new CallToolResult({
520
+ isError: true,
521
+ content: [
522
+ new TextContent({
523
+ text: JSON.stringify(error)
524
+ })
525
+ ]
526
+ }),
527
+ onSuccess: (result) =>
528
+ new CallToolResult({
529
+ isError: false,
530
+ content: [
531
+ new TextContent({
532
+ text: JSON.stringify(result.encodedResult)
533
+ })
534
+ ]
535
+ })
536
+ })
537
+ ) as any
538
+ }
539
+ })
540
+ }
541
+ })
542
+
543
+ /**
544
+ * Register an AiToolkit with the McpServer.
545
+ *
546
+ * @since 1.0.0
547
+ * @category Tools
548
+ */
549
+ export const toolkit = <Tools extends AiTool.Any>(
550
+ toolkit: AiToolkit.AiToolkit<Tools>
551
+ ): Layer.Layer<never, never, AiTool.ToHandler<Tools>> =>
552
+ Layer.effectDiscard(registerToolkit(toolkit)).pipe(
553
+ Layer.provide(McpServer.layer)
554
+ )
555
+
556
+ /**
557
+ * @since 1.0.0
558
+ */
559
+ export type ValidateCompletions<Completions, Keys extends string> =
560
+ & Completions
561
+ & {
562
+ readonly [K in keyof Completions]: K extends Keys ? (input: string) => any : never
563
+ }
564
+
565
+ /**
566
+ * @since 1.0.0
567
+ */
568
+ export type ResourceCompletions<Schemas extends ReadonlyArray<Schema.Schema.Any>> = {
569
+ readonly [
570
+ K in Extract<keyof Schemas, `${number}`> as Schemas[K] extends Param<infer Id, infer _S> ? Id
571
+ : `param${K}`
572
+ ]: (input: string) => Effect.Effect<Array<Schema.Schema.Type<Schemas[K]>>, any, any>
573
+ }
574
+
575
+ /**
576
+ * Register a resource with the McpServer.
577
+ *
578
+ * @since 1.0.0
579
+ * @category Resources
580
+ */
581
+ export const registerResource: {
582
+ /**
583
+ * Register a resource with the McpServer.
584
+ *
585
+ * @since 1.0.0
586
+ * @category Resources
587
+ */
588
+ <E, R>(
589
+ options: {
590
+ readonly uri: string
591
+ readonly name: string
592
+ readonly description?: string | undefined
593
+ readonly mimeType?: string | undefined
594
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
595
+ readonly priority?: number | undefined
596
+ readonly content: Effect.Effect<
597
+ ReadResourceResult | string | Uint8Array,
598
+ E,
599
+ R
600
+ >
601
+ }
602
+ ): Effect.Effect<void, never, R | McpServer>
603
+ /**
604
+ * Register a resource with the McpServer.
605
+ *
606
+ * @since 1.0.0
607
+ * @category Resources
608
+ */
609
+ <const Schemas extends ReadonlyArray<Schema.Schema.Any>>(
610
+ segments: TemplateStringsArray,
611
+ ...schemas:
612
+ & Schemas
613
+ & {
614
+ readonly [K in keyof Schemas]: Schema.Schema.Encoded<Schemas[K]> extends string ? unknown
615
+ : "Schema must be encodable to a string"
616
+ }
617
+ ): <
618
+ E,
619
+ R,
620
+ const Completions extends Partial<ResourceCompletions<Schemas>> = {}
621
+ >(options: {
622
+ readonly name: string
623
+ readonly description?: string | undefined
624
+ readonly mimeType?: string | undefined
625
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
626
+ readonly priority?: number | undefined
627
+ readonly completion?: ValidateCompletions<Completions, keyof ResourceCompletions<Schemas>> | undefined
628
+ readonly content: (uri: string, ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] }) => Effect.Effect<
629
+ ReadResourceResult | string | Uint8Array,
630
+ E,
631
+ R
632
+ >
633
+ }) => Effect.Effect<
634
+ void,
635
+ never,
636
+ | R
637
+ | (Completions[keyof Completions] extends (input: string) => infer Ret ?
638
+ Ret extends Effect.Effect<infer _A, infer _E, infer _R> ? _R : never
639
+ : never)
640
+ | McpServer
641
+ >
642
+ } = function() {
643
+ if (arguments.length === 1) {
644
+ const options = arguments[0] as Resource & Annotations & {
645
+ readonly content: Effect.Effect<ReadResourceResult | string | Uint8Array>
646
+ }
647
+ return Effect.gen(function*() {
648
+ const context = yield* Effect.context<any>()
649
+ const registry = yield* McpServer
650
+ yield* registry.addResource(
651
+ new Resource({
652
+ ...options,
653
+ annotations: new Annotations(options)
654
+ }),
655
+ options.content.pipe(
656
+ Effect.provide(context),
657
+ Effect.map((content) => resolveResourceContent(options.uri, content)),
658
+ Effect.catchAllCause((cause) => {
659
+ const prettyError = Cause.prettyErrors(cause)[0]
660
+ return new InternalError({ message: prettyError.message })
661
+ })
662
+ )
663
+ )
664
+ })
665
+ }
666
+ const {
667
+ params,
668
+ routerPath,
669
+ schema,
670
+ uriPath
671
+ } = compileUriTemplate(...(arguments as any as [any, any]))
672
+ return Effect.fnUntraced(function*<E, R>(options: {
673
+ readonly name: string
674
+ readonly description?: string | undefined
675
+ readonly mimeType?: string | undefined
676
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
677
+ readonly priority?: number | undefined
678
+ readonly completion?: Record<string, (input: string) => Effect.Effect<any>> | undefined
679
+ readonly content: (uri: string, ...params: Array<any>) => Effect.Effect<
680
+ ReadResourceResult | string | Uint8Array,
681
+ E,
682
+ R
683
+ >
684
+ }) {
685
+ const context = yield* Effect.context<any>()
686
+ const registry = yield* McpServer
687
+ const decode = Schema.decodeUnknown(schema)
688
+ const template = new ResourceTemplate({
689
+ ...options,
690
+ uriTemplate: uriPath,
691
+ annotations: new Annotations(options)
692
+ })
693
+ const completions: Record<string, (input: string) => Effect.Effect<CompleteResult, InternalError>> = {}
694
+ for (const [param, handle] of Object.entries(options.completion ?? {})) {
695
+ const encodeArray = Schema.encodeUnknown(Schema.Array(params[param]))
696
+ const handler = (input: string) =>
697
+ handle(input).pipe(
698
+ Effect.flatMap(encodeArray),
699
+ Effect.map((values) =>
700
+ new CompleteResult({
701
+ completion: {
702
+ values: values as Array<string>,
703
+ total: values.length,
704
+ hasMore: false
705
+ }
706
+ })
707
+ ),
708
+ Effect.catchAllCause((cause) => {
709
+ const prettyError = Cause.prettyErrors(cause)[0]
710
+ return new InternalError({ message: prettyError.message })
711
+ }),
712
+ Effect.provide(context)
713
+ )
714
+ completions[param] = handler
715
+ }
716
+ yield* registry.addResourceTemplate({
717
+ template,
718
+ routerPath,
719
+ completions,
720
+ handle: (uri, params) =>
721
+ decode(params).pipe(
722
+ Effect.mapError((error) => new InvalidParams({ message: error.message })),
723
+ Effect.flatMap((params) =>
724
+ options.content(uri, ...params).pipe(
725
+ Effect.map((content) => resolveResourceContent(uri, content)),
726
+ Effect.catchAllCause((cause) => {
727
+ const prettyError = Cause.prettyErrors(cause)[0]
728
+ return new InternalError({ message: prettyError.message })
729
+ })
730
+ )
731
+ ),
732
+ Effect.provide(context)
733
+ )
734
+ })
735
+ })
736
+ } as any
737
+
738
+ /**
739
+ * Register a resource with the McpServer.
740
+ *
741
+ * @since 1.0.0
742
+ * @category Resources
743
+ */
744
+ export const resource: {
745
+ /**
746
+ * Register a resource with the McpServer.
747
+ *
748
+ * @since 1.0.0
749
+ * @category Resources
750
+ */
751
+ <E, R>(
752
+ options: {
753
+ readonly uri: string
754
+ readonly name: string
755
+ readonly description?: string | undefined
756
+ readonly mimeType?: string | undefined
757
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
758
+ readonly priority?: number | undefined
759
+ readonly content: Effect.Effect<
760
+ ReadResourceResult | string | Uint8Array,
761
+ E,
762
+ R
763
+ >
764
+ }
765
+ ): Layer.Layer<never, never, R | McpServer>
766
+ /**
767
+ * Register a resource with the McpServer.
768
+ *
769
+ * @since 1.0.0
770
+ * @category Resources
771
+ */
772
+ <const Schemas extends ReadonlyArray<Schema.Schema.Any>>(
773
+ segments: TemplateStringsArray,
774
+ ...schemas:
775
+ & Schemas
776
+ & {
777
+ readonly [K in keyof Schemas]: Schema.Schema.Encoded<Schemas[K]> extends string ? unknown
778
+ : "Schema must be encodable to a string"
779
+ }
780
+ ): <
781
+ E,
782
+ R,
783
+ const Completions extends Partial<ResourceCompletions<Schemas>> = {}
784
+ >(options: {
785
+ readonly name: string
786
+ readonly description?: string | undefined
787
+ readonly mimeType?: string | undefined
788
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
789
+ readonly priority?: number | undefined
790
+ readonly completion?: ValidateCompletions<Completions, keyof ResourceCompletions<Schemas>> | undefined
791
+ readonly content: (uri: string, ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] }) => Effect.Effect<
792
+ ReadResourceResult | string | Uint8Array,
793
+ E,
794
+ R
795
+ >
796
+ }) => Layer.Layer<
797
+ never,
798
+ never,
799
+ | R
800
+ | (Completions[keyof Completions] extends (input: string) => infer Ret ?
801
+ Ret extends Effect.Effect<infer _A, infer _E, infer _R> ? _R : never
802
+ : never)
803
+ >
804
+ } = function() {
805
+ if (arguments.length === 1) {
806
+ return Layer.effectDiscard(registerResource(arguments[0])).pipe(
807
+ Layer.provide(McpServer.layer)
808
+ )
809
+ }
810
+ const register = registerResource(...(arguments as any as [any, any]))
811
+ return (options: any) =>
812
+ Layer.effectDiscard(register(options)).pipe(
813
+ Layer.provide(McpServer.layer)
814
+ )
815
+ } as any
816
+
817
+ /**
818
+ * Register a prompt with the McpServer.
819
+ *
820
+ * @since 1.0.0
821
+ * @category Resources
822
+ */
823
+ export const registerPrompt = <
824
+ E,
825
+ R,
826
+ Params = {},
827
+ ParamsI extends Record<string, string> = {},
828
+ ParamsR = never,
829
+ const Completions extends {
830
+ readonly [K in keyof Params]?: (input: string) => Effect.Effect<Array<Params[K]>, any, any>
831
+ } = {}
832
+ >(
833
+ options: {
834
+ readonly name: string
835
+ readonly description?: string | undefined
836
+ readonly parameters?: Schema.Schema<Params, ParamsI, ParamsR> | undefined
837
+ readonly completion?: ValidateCompletions<Completions, Extract<keyof Params, string>> | undefined
838
+ readonly content: (params: Params) => Effect.Effect<Array<PromptMessage> | string, E, R>
839
+ }
840
+ ): Effect.Effect<void, never, ParamsR | R | McpServer> => {
841
+ const args = Arr.empty<PromptArgument>()
842
+ const props: Record<string, Schema.Schema.Any> = {}
843
+ const propSignatures = options.parameters ? AST.getPropertySignatures(options.parameters.ast) : []
844
+ for (const prop of propSignatures) {
845
+ args.push(
846
+ new PromptArgument({
847
+ name: prop.name as string,
848
+ description: Option.getOrUndefined(AST.getDescriptionAnnotation(prop)),
849
+ required: !prop.isOptional
850
+ })
851
+ )
852
+ props[prop.name as string] = Schema.make(prop.type)
853
+ }
854
+ const prompt = new Prompt({
855
+ name: options.name,
856
+ description: options.description,
857
+ arguments: args
858
+ })
859
+ const decode = options.parameters ? Schema.decodeUnknown(options.parameters) : () => Effect.succeed({} as Params)
860
+ const completion: Record<string, (input: string) => Effect.Effect<any>> = options.completion ?? {}
861
+ return Effect.gen(function*() {
862
+ const registry = yield* McpServer
863
+ const context = yield* Effect.context<R | ParamsR>()
864
+ const completions: Record<string, (input: string) => Effect.Effect<CompleteResult, InternalError>> = {}
865
+ for (const [param, handle] of Object.entries(completion)) {
866
+ const encodeArray = Schema.encodeUnknown(Schema.Array(props[param]))
867
+ const handler = (input: string) =>
868
+ handle(input).pipe(
869
+ Effect.flatMap(encodeArray),
870
+ Effect.map((values) =>
871
+ new CompleteResult({
872
+ completion: {
873
+ values: values as Array<string>,
874
+ total: values.length,
875
+ hasMore: false
876
+ }
877
+ })
878
+ ),
879
+ Effect.catchAllCause((cause) => {
880
+ const prettyError = Cause.prettyErrors(cause)[0]
881
+ return new InternalError({ message: prettyError.message })
882
+ }),
883
+ Effect.provide(context)
884
+ )
885
+ completions[param] = handler as any
886
+ }
887
+ yield* registry.addPrompt({
888
+ prompt,
889
+ completions,
890
+ handle: (params) =>
891
+ decode(params).pipe(
892
+ Effect.mapError((error) => new InvalidParams({ message: error.message })),
893
+ Effect.flatMap((params) => options.content(params)),
894
+ Effect.map((messages) => {
895
+ messages = typeof messages === "string" ?
896
+ [
897
+ new PromptMessage({
898
+ role: "user",
899
+ content: new TextContent({ text: messages })
900
+ })
901
+ ] :
902
+ messages
903
+ return new GetPromptResult({ messages, description: prompt.description })
904
+ }),
905
+ Effect.catchAllCause((cause) => {
906
+ const prettyError = Cause.prettyErrors(cause)[0]
907
+ return new InternalError({ message: prettyError.message })
908
+ }),
909
+ Effect.provide(context)
910
+ )
911
+ })
912
+ })
913
+ }
914
+
915
+ /**
916
+ * Register a prompt with the McpServer.
917
+ *
918
+ * @since 1.0.0
919
+ * @category Resources
920
+ */
921
+ export const prompt = <
922
+ E,
923
+ R,
924
+ Params = {},
925
+ ParamsI extends Record<string, string> = {},
926
+ ParamsR = never,
927
+ const Completions extends {
928
+ readonly [K in keyof Params]?: (input: string) => Effect.Effect<Array<Params[K]>, any, any>
929
+ } = {}
930
+ >(
931
+ options: {
932
+ readonly name: string
933
+ readonly description?: string | undefined
934
+ readonly parameters?: Schema.Schema<Params, ParamsI, ParamsR> | undefined
935
+ readonly completion?: ValidateCompletions<Completions, Extract<keyof Params, string>> | undefined
936
+ readonly content: (params: Params) => Effect.Effect<Array<PromptMessage> | string, E, R>
937
+ }
938
+ ): Layer.Layer<never, never, ParamsR | R> =>
939
+ Layer.effectDiscard(registerPrompt(options)).pipe(
940
+ Layer.provide(McpServer.layer)
941
+ )
942
+
943
+ // -----------------------------------------------------------------------------
944
+ // Internal
945
+ // -----------------------------------------------------------------------------
946
+
947
+ const makeUriMatcher = <A>() => {
948
+ const router = FindMyWay.make<A>({
949
+ ignoreTrailingSlash: true,
950
+ ignoreDuplicateSlashes: true,
951
+ caseSensitive: true
952
+ })
953
+ const add = (uri: string, value: A) => {
954
+ router.on("GET", uri as any, value)
955
+ }
956
+ const find = (uri: string) => router.find("GET", uri)
957
+
958
+ return { add, find } as const
959
+ }
960
+
961
+ const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: ReadonlyArray<Schema.Schema.Any>) => {
962
+ let routerPath = segments[0].replace(":", "::")
963
+ let uriPath = segments[0]
964
+ const params: Record<string, Schema.Schema.Any> = {}
965
+ let pathSchema = Schema.Tuple() as Schema.Schema.Any
966
+ if (schemas.length > 0) {
967
+ const arr: Array<Schema.Schema.Any> = []
968
+ for (let i = 0; i < schemas.length; i++) {
969
+ const schema = schemas[i]
970
+ const key = String(i)
971
+ arr.push(schema)
972
+ routerPath += `:${key}${segments[i + 1].replace(":", "::")}`
973
+ const paramName = AST.getAnnotation(ParamAnnotation)(schema.ast).pipe(
974
+ Option.getOrElse(() => `param${key}`)
975
+ )
976
+ params[paramName as string] = schema
977
+ uriPath += `{${paramName}}`
978
+ }
979
+ pathSchema = Schema.Tuple(...arr)
980
+ }
981
+ return {
982
+ routerPath,
983
+ uriPath,
984
+ schema: pathSchema,
985
+ params
986
+ } as const
987
+ }
988
+
989
+ const layerHandlers = (serverInfo: {
990
+ readonly name: string
991
+ readonly version: string
992
+ }) =>
993
+ ClientRpcs.toLayer(
994
+ Effect.gen(function*() {
995
+ const server = yield* McpServer
996
+
997
+ return {
998
+ // Requests
999
+ ping: () => Effect.succeed({}),
1000
+ initialize(params) {
1001
+ const requestedVersion = params.protocolVersion
1002
+ const capabilities: Types.DeepMutable<ServerCapabilities> = {
1003
+ completions: {}
1004
+ }
1005
+ if (server.tools.length > 0) {
1006
+ capabilities.tools = { listChanged: true }
1007
+ }
1008
+ if (server.resources.length > 0 || server.resourceTemplates.length > 0) {
1009
+ capabilities.resources = {
1010
+ listChanged: true,
1011
+ subscribe: false
1012
+ }
1013
+ }
1014
+ if (server.prompts.length > 0) {
1015
+ capabilities.prompts = { listChanged: true }
1016
+ }
1017
+ return Effect.succeed({
1018
+ capabilities,
1019
+ serverInfo,
1020
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
1021
+ ? requestedVersion
1022
+ : LATEST_PROTOCOL_VERSION
1023
+ })
1024
+ },
1025
+ "completion/complete": server.completion,
1026
+ "logging/setLevel": () => InternalError.notImplemented,
1027
+ "prompts/get": server.getPromptResult,
1028
+ "prompts/list": () => Effect.sync(() => new ListPromptsResult({ prompts: server.prompts })),
1029
+ "resources/list": () => Effect.sync(() => new ListResourcesResult({ resources: server.resources })),
1030
+ "resources/read": ({ uri }) => server.findResource(uri),
1031
+ "resources/subscribe": () => InternalError.notImplemented,
1032
+ "resources/unsubscribe": () => InternalError.notImplemented,
1033
+ "resources/templates/list": () =>
1034
+ Effect.sync(() => new ListResourceTemplatesResult({ resourceTemplates: server.resourceTemplates })),
1035
+ "tools/call": server.callTool,
1036
+ "tools/list": () => Effect.sync(() => new ListToolsResult({ tools: server.tools })),
1037
+
1038
+ // Notifications
1039
+ "notifications/cancelled": (_) => Effect.void,
1040
+ "notifications/initialized": (_) => Effect.void,
1041
+ "notifications/progress": (_) => Effect.void,
1042
+ "notifications/roots/list_changed": (_) => Effect.void
1043
+ }
1044
+ })
1045
+ )
1046
+
1047
+ const makeJsonSchema = (ast: AST.AST): JsonSchema.JsonSchema7 => {
1048
+ const props = AST.getPropertySignatures(ast)
1049
+ if (props.length === 0) {
1050
+ return {
1051
+ type: "object",
1052
+ properties: {},
1053
+ required: [],
1054
+ additionalProperties: false
1055
+ }
1056
+ }
1057
+ const $defs = {}
1058
+ const schema = JsonSchema.fromAST(ast, {
1059
+ definitions: $defs,
1060
+ topLevelReferenceStrategy: "skip"
1061
+ })
1062
+ if (Object.keys($defs).length === 0) return schema
1063
+ ;(schema as any).$defs = $defs
1064
+ return schema
1065
+ }
1066
+
1067
+ const resolveResourceContent = (uri: string, content: ReadResourceResult | string | Uint8Array) => {
1068
+ if (typeof content === "string") {
1069
+ return new ReadResourceResult({
1070
+ contents: [
1071
+ new TextResourceContents({
1072
+ uri,
1073
+ text: content
1074
+ })
1075
+ ]
1076
+ })
1077
+ } else if (content instanceof Uint8Array) {
1078
+ return new ReadResourceResult({
1079
+ contents: [
1080
+ new BlobResourceContents({
1081
+ uri,
1082
+ blob: content
1083
+ })
1084
+ ]
1085
+ })
1086
+ }
1087
+ return content
1088
+ }