@effect/ai 0.18.12 → 0.18.13

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,1078 @@
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> | McpServer> => Layer.effectDiscard(registerToolkit(toolkit))
552
+
553
+ /**
554
+ * @since 1.0.0
555
+ */
556
+ export type ValidateCompletions<Completions, Keys extends string> =
557
+ & Completions
558
+ & {
559
+ readonly [K in keyof Completions]: K extends Keys ? (input: string) => any : never
560
+ }
561
+
562
+ /**
563
+ * @since 1.0.0
564
+ */
565
+ export type ResourceCompletions<Schemas extends ReadonlyArray<Schema.Schema.Any>> = {
566
+ readonly [
567
+ K in Extract<keyof Schemas, `${number}`> as Schemas[K] extends Param<infer Id, infer _S> ? Id
568
+ : `param${K}`
569
+ ]: (input: string) => Effect.Effect<Array<Schema.Schema.Type<Schemas[K]>>, any, any>
570
+ }
571
+
572
+ /**
573
+ * Register a resource with the McpServer.
574
+ *
575
+ * @since 1.0.0
576
+ * @category Resources
577
+ */
578
+ export const registerResource: {
579
+ /**
580
+ * Register a resource with the McpServer.
581
+ *
582
+ * @since 1.0.0
583
+ * @category Resources
584
+ */
585
+ <E, R>(
586
+ options: {
587
+ readonly uri: string
588
+ readonly name: string
589
+ readonly description?: string | undefined
590
+ readonly mimeType?: string | undefined
591
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
592
+ readonly priority?: number | undefined
593
+ readonly content: Effect.Effect<
594
+ ReadResourceResult | string | Uint8Array,
595
+ E,
596
+ R
597
+ >
598
+ }
599
+ ): Effect.Effect<void, never, R | McpServer>
600
+ /**
601
+ * Register a resource with the McpServer.
602
+ *
603
+ * @since 1.0.0
604
+ * @category Resources
605
+ */
606
+ <const Schemas extends ReadonlyArray<Schema.Schema.Any>>(
607
+ segments: TemplateStringsArray,
608
+ ...schemas:
609
+ & Schemas
610
+ & {
611
+ readonly [K in keyof Schemas]: Schema.Schema.Encoded<Schemas[K]> extends string ? unknown
612
+ : "Schema must be encodable to a string"
613
+ }
614
+ ): <
615
+ E,
616
+ R,
617
+ const Completions extends Partial<ResourceCompletions<Schemas>> = {}
618
+ >(options: {
619
+ readonly name: string
620
+ readonly description?: string | undefined
621
+ readonly mimeType?: string | undefined
622
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
623
+ readonly priority?: number | undefined
624
+ readonly completion?: ValidateCompletions<Completions, keyof ResourceCompletions<Schemas>> | undefined
625
+ readonly content: (uri: string, ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] }) => Effect.Effect<
626
+ ReadResourceResult | string | Uint8Array,
627
+ E,
628
+ R
629
+ >
630
+ }) => Effect.Effect<
631
+ void,
632
+ never,
633
+ | R
634
+ | (Completions[keyof Completions] extends (input: string) => infer Ret ?
635
+ Ret extends Effect.Effect<infer _A, infer _E, infer _R> ? _R : never
636
+ : never)
637
+ | McpServer
638
+ >
639
+ } = function() {
640
+ if (arguments.length === 1) {
641
+ const options = arguments[0] as Resource & Annotations & {
642
+ readonly content: Effect.Effect<ReadResourceResult | string | Uint8Array>
643
+ }
644
+ return Effect.gen(function*() {
645
+ const context = yield* Effect.context<any>()
646
+ const registry = yield* McpServer
647
+ yield* registry.addResource(
648
+ new Resource({
649
+ ...options,
650
+ annotations: new Annotations(options)
651
+ }),
652
+ options.content.pipe(
653
+ Effect.provide(context),
654
+ Effect.map((content) => resolveResourceContent(options.uri, content)),
655
+ Effect.catchAllCause((cause) => {
656
+ const prettyError = Cause.prettyErrors(cause)[0]
657
+ return new InternalError({ message: prettyError.message })
658
+ })
659
+ )
660
+ )
661
+ })
662
+ }
663
+ const {
664
+ params,
665
+ routerPath,
666
+ schema,
667
+ uriPath
668
+ } = compileUriTemplate(...(arguments as any as [any, any]))
669
+ return Effect.fnUntraced(function*<E, R>(options: {
670
+ readonly name: string
671
+ readonly description?: string | undefined
672
+ readonly mimeType?: string | undefined
673
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
674
+ readonly priority?: number | undefined
675
+ readonly completion?: Record<string, (input: string) => Effect.Effect<any>> | undefined
676
+ readonly content: (uri: string, ...params: Array<any>) => Effect.Effect<
677
+ ReadResourceResult | string | Uint8Array,
678
+ E,
679
+ R
680
+ >
681
+ }) {
682
+ const context = yield* Effect.context<any>()
683
+ const registry = yield* McpServer
684
+ const decode = Schema.decodeUnknown(schema)
685
+ const template = new ResourceTemplate({
686
+ ...options,
687
+ uriTemplate: uriPath,
688
+ annotations: new Annotations(options)
689
+ })
690
+ const completions: Record<string, (input: string) => Effect.Effect<CompleteResult, InternalError>> = {}
691
+ for (const [param, handle] of Object.entries(options.completion ?? {})) {
692
+ const encodeArray = Schema.encodeUnknown(Schema.Array(params[param]))
693
+ const handler = (input: string) =>
694
+ handle(input).pipe(
695
+ Effect.flatMap(encodeArray),
696
+ Effect.map((values) =>
697
+ new CompleteResult({
698
+ completion: {
699
+ values: values as Array<string>,
700
+ total: values.length,
701
+ hasMore: false
702
+ }
703
+ })
704
+ ),
705
+ Effect.catchAllCause((cause) => {
706
+ const prettyError = Cause.prettyErrors(cause)[0]
707
+ return new InternalError({ message: prettyError.message })
708
+ }),
709
+ Effect.provide(context)
710
+ )
711
+ completions[param] = handler
712
+ }
713
+ yield* registry.addResourceTemplate({
714
+ template,
715
+ routerPath,
716
+ completions,
717
+ handle: (uri, params) =>
718
+ decode(params).pipe(
719
+ Effect.mapError((error) => new InvalidParams({ message: error.message })),
720
+ Effect.flatMap((params) =>
721
+ options.content(uri, ...params).pipe(
722
+ Effect.map((content) => resolveResourceContent(uri, content)),
723
+ Effect.catchAllCause((cause) => {
724
+ const prettyError = Cause.prettyErrors(cause)[0]
725
+ return new InternalError({ message: prettyError.message })
726
+ })
727
+ )
728
+ ),
729
+ Effect.provide(context)
730
+ )
731
+ })
732
+ })
733
+ } as any
734
+
735
+ /**
736
+ * Register a resource with the McpServer.
737
+ *
738
+ * @since 1.0.0
739
+ * @category Resources
740
+ */
741
+ export const resource: {
742
+ /**
743
+ * Register a resource with the McpServer.
744
+ *
745
+ * @since 1.0.0
746
+ * @category Resources
747
+ */
748
+ <E, R>(
749
+ options: {
750
+ readonly uri: string
751
+ readonly name: string
752
+ readonly description?: string | undefined
753
+ readonly mimeType?: string | undefined
754
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
755
+ readonly priority?: number | undefined
756
+ readonly content: Effect.Effect<
757
+ ReadResourceResult | string | Uint8Array,
758
+ E,
759
+ R
760
+ >
761
+ }
762
+ ): Layer.Layer<never, never, R | McpServer>
763
+ /**
764
+ * Register a resource with the McpServer.
765
+ *
766
+ * @since 1.0.0
767
+ * @category Resources
768
+ */
769
+ <const Schemas extends ReadonlyArray<Schema.Schema.Any>>(
770
+ segments: TemplateStringsArray,
771
+ ...schemas:
772
+ & Schemas
773
+ & {
774
+ readonly [K in keyof Schemas]: Schema.Schema.Encoded<Schemas[K]> extends string ? unknown
775
+ : "Schema must be encodable to a string"
776
+ }
777
+ ): <
778
+ E,
779
+ R,
780
+ const Completions extends Partial<ResourceCompletions<Schemas>> = {}
781
+ >(options: {
782
+ readonly name: string
783
+ readonly description?: string | undefined
784
+ readonly mimeType?: string | undefined
785
+ readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
786
+ readonly priority?: number | undefined
787
+ readonly completion?: ValidateCompletions<Completions, keyof ResourceCompletions<Schemas>> | undefined
788
+ readonly content: (uri: string, ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] }) => Effect.Effect<
789
+ ReadResourceResult | string | Uint8Array,
790
+ E,
791
+ R
792
+ >
793
+ }) => Layer.Layer<
794
+ never,
795
+ never,
796
+ | McpServer
797
+ | R
798
+ | (Completions[keyof Completions] extends (input: string) => infer Ret ?
799
+ Ret extends Effect.Effect<infer _A, infer _E, infer _R> ? _R : never
800
+ : never)
801
+ >
802
+ } = function() {
803
+ if (arguments.length === 1) {
804
+ return Layer.effectDiscard(registerResource(arguments[0]))
805
+ }
806
+ const register = registerResource(...(arguments as any as [any, any]))
807
+ return (options: any) => Layer.effectDiscard(register(options))
808
+ } as any
809
+
810
+ /**
811
+ * Register a prompt with the McpServer.
812
+ *
813
+ * @since 1.0.0
814
+ * @category Resources
815
+ */
816
+ export const registerPrompt = <
817
+ E,
818
+ R,
819
+ Params = {},
820
+ ParamsI extends Record<string, string> = {},
821
+ ParamsR = never,
822
+ const Completions extends {
823
+ readonly [K in keyof Params]?: (input: string) => Effect.Effect<Array<Params[K]>, any, any>
824
+ } = {}
825
+ >(
826
+ options: {
827
+ readonly name: string
828
+ readonly description?: string | undefined
829
+ readonly parameters?: Schema.Schema<Params, ParamsI, ParamsR> | undefined
830
+ readonly completion?: ValidateCompletions<Completions, Extract<keyof Params, string>> | undefined
831
+ readonly content: (params: Params) => Effect.Effect<Array<PromptMessage> | string, E, R>
832
+ }
833
+ ): Effect.Effect<void, never, ParamsR | R | McpServer> => {
834
+ const args = Arr.empty<PromptArgument>()
835
+ const props: Record<string, Schema.Schema.Any> = {}
836
+ const propSignatures = options.parameters ? AST.getPropertySignatures(options.parameters.ast) : []
837
+ for (const prop of propSignatures) {
838
+ args.push(
839
+ new PromptArgument({
840
+ name: prop.name as string,
841
+ description: Option.getOrUndefined(AST.getDescriptionAnnotation(prop)),
842
+ required: !prop.isOptional
843
+ })
844
+ )
845
+ props[prop.name as string] = Schema.make(prop.type)
846
+ }
847
+ const prompt = new Prompt({
848
+ name: options.name,
849
+ description: options.description,
850
+ arguments: args
851
+ })
852
+ const decode = options.parameters ? Schema.decodeUnknown(options.parameters) : () => Effect.succeed({} as Params)
853
+ const completion: Record<string, (input: string) => Effect.Effect<any>> = options.completion ?? {}
854
+ return Effect.gen(function*() {
855
+ const registry = yield* McpServer
856
+ const context = yield* Effect.context<R | ParamsR>()
857
+ const completions: Record<string, (input: string) => Effect.Effect<CompleteResult, InternalError>> = {}
858
+ for (const [param, handle] of Object.entries(completion)) {
859
+ const encodeArray = Schema.encodeUnknown(Schema.Array(props[param]))
860
+ const handler = (input: string) =>
861
+ handle(input).pipe(
862
+ Effect.flatMap(encodeArray),
863
+ Effect.map((values) =>
864
+ new CompleteResult({
865
+ completion: {
866
+ values: values as Array<string>,
867
+ total: values.length,
868
+ hasMore: false
869
+ }
870
+ })
871
+ ),
872
+ Effect.catchAllCause((cause) => {
873
+ const prettyError = Cause.prettyErrors(cause)[0]
874
+ return new InternalError({ message: prettyError.message })
875
+ }),
876
+ Effect.provide(context)
877
+ )
878
+ completions[param] = handler as any
879
+ }
880
+ yield* registry.addPrompt({
881
+ prompt,
882
+ completions,
883
+ handle: (params) =>
884
+ decode(params).pipe(
885
+ Effect.mapError((error) => new InvalidParams({ message: error.message })),
886
+ Effect.flatMap((params) => options.content(params)),
887
+ Effect.map((messages) => {
888
+ messages = typeof messages === "string" ?
889
+ [
890
+ new PromptMessage({
891
+ role: "user",
892
+ content: new TextContent({ text: messages })
893
+ })
894
+ ] :
895
+ messages
896
+ return new GetPromptResult({ messages, description: prompt.description })
897
+ }),
898
+ Effect.catchAllCause((cause) => {
899
+ const prettyError = Cause.prettyErrors(cause)[0]
900
+ return new InternalError({ message: prettyError.message })
901
+ }),
902
+ Effect.provide(context)
903
+ )
904
+ })
905
+ })
906
+ }
907
+
908
+ /**
909
+ * Register a prompt with the McpServer.
910
+ *
911
+ * @since 1.0.0
912
+ * @category Resources
913
+ */
914
+ export const prompt = <
915
+ E,
916
+ R,
917
+ Params = {},
918
+ ParamsI extends Record<string, string> = {},
919
+ ParamsR = never,
920
+ const Completions extends {
921
+ readonly [K in keyof Params]?: (input: string) => Effect.Effect<Array<Params[K]>, any, any>
922
+ } = {}
923
+ >(
924
+ options: {
925
+ readonly name: string
926
+ readonly description?: string | undefined
927
+ readonly parameters?: Schema.Schema<Params, ParamsI, ParamsR> | undefined
928
+ readonly completion?: ValidateCompletions<Completions, Extract<keyof Params, string>> | undefined
929
+ readonly content: (params: Params) => Effect.Effect<Array<PromptMessage> | string, E, R>
930
+ }
931
+ ): Layer.Layer<never, never, ParamsR | R | McpServer> => Layer.effectDiscard(registerPrompt(options))
932
+
933
+ // -----------------------------------------------------------------------------
934
+ // Internal
935
+ // -----------------------------------------------------------------------------
936
+
937
+ const makeUriMatcher = <A>() => {
938
+ const router = FindMyWay.make<A>({
939
+ ignoreTrailingSlash: true,
940
+ ignoreDuplicateSlashes: true,
941
+ caseSensitive: true
942
+ })
943
+ const add = (uri: string, value: A) => {
944
+ router.on("GET", uri as any, value)
945
+ }
946
+ const find = (uri: string) => router.find("GET", uri)
947
+
948
+ return { add, find } as const
949
+ }
950
+
951
+ const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: ReadonlyArray<Schema.Schema.Any>) => {
952
+ let routerPath = segments[0].replace(":", "::")
953
+ let uriPath = segments[0]
954
+ const params: Record<string, Schema.Schema.Any> = {}
955
+ let pathSchema = Schema.Tuple() as Schema.Schema.Any
956
+ if (schemas.length > 0) {
957
+ const arr: Array<Schema.Schema.Any> = []
958
+ for (let i = 0; i < schemas.length; i++) {
959
+ const schema = schemas[i]
960
+ const key = String(i)
961
+ arr.push(schema)
962
+ routerPath += `:${key}${segments[i + 1].replace(":", "::")}`
963
+ const paramName = AST.getAnnotation(ParamAnnotation)(schema.ast).pipe(
964
+ Option.getOrElse(() => `param${key}`)
965
+ )
966
+ params[paramName as string] = schema
967
+ uriPath += `{${paramName}}`
968
+ }
969
+ pathSchema = Schema.Tuple(...arr)
970
+ }
971
+ return {
972
+ routerPath,
973
+ uriPath,
974
+ schema: pathSchema,
975
+ params
976
+ } as const
977
+ }
978
+
979
+ const layerHandlers = (serverInfo: {
980
+ readonly name: string
981
+ readonly version: string
982
+ }) =>
983
+ ClientRpcs.toLayer(
984
+ Effect.gen(function*() {
985
+ const server = yield* McpServer
986
+
987
+ return {
988
+ // Requests
989
+ ping: () => Effect.succeed({}),
990
+ initialize(params) {
991
+ const requestedVersion = params.protocolVersion
992
+ const capabilities: Types.DeepMutable<ServerCapabilities> = {
993
+ completions: {}
994
+ }
995
+ if (server.tools.length > 0) {
996
+ capabilities.tools = { listChanged: true }
997
+ }
998
+ if (server.resources.length > 0 || server.resourceTemplates.length > 0) {
999
+ capabilities.resources = {
1000
+ listChanged: true,
1001
+ subscribe: false
1002
+ }
1003
+ }
1004
+ if (server.prompts.length > 0) {
1005
+ capabilities.prompts = { listChanged: true }
1006
+ }
1007
+ return Effect.succeed({
1008
+ capabilities,
1009
+ serverInfo,
1010
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
1011
+ ? requestedVersion
1012
+ : LATEST_PROTOCOL_VERSION
1013
+ })
1014
+ },
1015
+ "completion/complete": server.completion,
1016
+ "logging/setLevel": () => InternalError.notImplemented,
1017
+ "prompts/get": server.getPromptResult,
1018
+ "prompts/list": () => Effect.sync(() => new ListPromptsResult({ prompts: server.prompts })),
1019
+ "resources/list": () => Effect.sync(() => new ListResourcesResult({ resources: server.resources })),
1020
+ "resources/read": ({ uri }) => server.findResource(uri),
1021
+ "resources/subscribe": () => InternalError.notImplemented,
1022
+ "resources/unsubscribe": () => InternalError.notImplemented,
1023
+ "resources/templates/list": () =>
1024
+ Effect.sync(() => new ListResourceTemplatesResult({ resourceTemplates: server.resourceTemplates })),
1025
+ "tools/call": server.callTool,
1026
+ "tools/list": () => Effect.sync(() => new ListToolsResult({ tools: server.tools })),
1027
+
1028
+ // Notifications
1029
+ "notifications/cancelled": (_) => Effect.void,
1030
+ "notifications/initialized": (_) => Effect.void,
1031
+ "notifications/progress": (_) => Effect.void,
1032
+ "notifications/roots/list_changed": (_) => Effect.void
1033
+ }
1034
+ })
1035
+ )
1036
+
1037
+ const makeJsonSchema = (ast: AST.AST): JsonSchema.JsonSchema7 => {
1038
+ const props = AST.getPropertySignatures(ast)
1039
+ if (props.length === 0) {
1040
+ return {
1041
+ type: "object",
1042
+ properties: {},
1043
+ required: [],
1044
+ additionalProperties: false
1045
+ }
1046
+ }
1047
+ const $defs = {}
1048
+ const schema = JsonSchema.fromAST(ast, {
1049
+ definitions: $defs,
1050
+ topLevelReferenceStrategy: "skip"
1051
+ })
1052
+ if (Object.keys($defs).length === 0) return schema
1053
+ ;(schema as any).$defs = $defs
1054
+ return schema
1055
+ }
1056
+
1057
+ const resolveResourceContent = (uri: string, content: ReadResourceResult | string | Uint8Array) => {
1058
+ if (typeof content === "string") {
1059
+ return new ReadResourceResult({
1060
+ contents: [
1061
+ new TextResourceContents({
1062
+ uri,
1063
+ text: content
1064
+ })
1065
+ ]
1066
+ })
1067
+ } else if (content instanceof Uint8Array) {
1068
+ return new ReadResourceResult({
1069
+ contents: [
1070
+ new BlobResourceContents({
1071
+ uri,
1072
+ blob: content
1073
+ })
1074
+ ]
1075
+ })
1076
+ }
1077
+ return content
1078
+ }