@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.
- package/McpSchema/package.json +6 -0
- package/McpServer/package.json +6 -0
- package/dist/cjs/AiTool.js +61 -3
- package/dist/cjs/AiTool.js.map +1 -1
- package/dist/cjs/McpSchema.js +1555 -0
- package/dist/cjs/McpSchema.js.map +1 -0
- package/dist/cjs/McpServer.js +708 -0
- package/dist/cjs/McpServer.js.map +1 -0
- package/dist/cjs/index.js +5 -1
- package/dist/dts/AiTool.d.ts +50 -4
- package/dist/dts/AiTool.d.ts.map +1 -1
- package/dist/dts/McpSchema.d.ts +2527 -0
- package/dist/dts/McpSchema.d.ts.map +1 -0
- package/dist/dts/McpServer.d.ts +366 -0
- package/dist/dts/McpServer.d.ts.map +1 -0
- package/dist/dts/index.d.ts +8 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/AiTool.js +54 -2
- package/dist/esm/AiTool.js.map +1 -1
- package/dist/esm/McpSchema.js +1471 -0
- package/dist/esm/McpSchema.js.map +1 -0
- package/dist/esm/McpServer.js +691 -0
- package/dist/esm/McpServer.js.map +1 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +24 -5
- package/src/AiTool.ts +89 -6
- package/src/McpSchema.ts +1903 -0
- package/src/McpServer.ts +1078 -0
- package/src/index.ts +10 -0
package/src/McpServer.ts
ADDED
|
@@ -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
|
+
}
|