@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
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
import * as Headers from "@effect/platform/Headers";
|
|
5
|
+
import * as RpcClient from "@effect/rpc/RpcClient";
|
|
6
|
+
import * as RpcSerialization from "@effect/rpc/RpcSerialization";
|
|
7
|
+
import * as RpcServer from "@effect/rpc/RpcServer";
|
|
8
|
+
import * as Arr from "effect/Array";
|
|
9
|
+
import * as Cause from "effect/Cause";
|
|
10
|
+
import * as Context from "effect/Context";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Exit from "effect/Exit";
|
|
13
|
+
import * as JsonSchema from "effect/JSONSchema";
|
|
14
|
+
import * as Layer from "effect/Layer";
|
|
15
|
+
import * as Logger from "effect/Logger";
|
|
16
|
+
import * as Mailbox from "effect/Mailbox";
|
|
17
|
+
import * as Option from "effect/Option";
|
|
18
|
+
import * as Schema from "effect/Schema";
|
|
19
|
+
import * as AST from "effect/SchemaAST";
|
|
20
|
+
import * as FindMyWay from "find-my-way-ts";
|
|
21
|
+
import * as AiTool from "./AiTool.js";
|
|
22
|
+
import { Annotations, BlobResourceContents, CallToolResult, ClientRpcs, CompleteResult, GetPromptResult, InternalError, InvalidParams, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, ListToolsResult, ParamAnnotation, Prompt, PromptArgument, PromptMessage, ReadResourceResult, Resource, ResourceTemplate, ServerNotificationRpcs, TextContent, TextResourceContents, Tool, ToolAnnotations } from "./McpSchema.js";
|
|
23
|
+
/**
|
|
24
|
+
* @since 1.0.0
|
|
25
|
+
* @category McpServer
|
|
26
|
+
*/
|
|
27
|
+
export class McpServer extends /*#__PURE__*/Context.Tag("@effect/ai/McpServer")() {
|
|
28
|
+
/**
|
|
29
|
+
* @since 1.0.0
|
|
30
|
+
*/
|
|
31
|
+
static make = /*#__PURE__*/Effect.gen(function* () {
|
|
32
|
+
const matcher = makeUriMatcher();
|
|
33
|
+
const tools = Arr.empty();
|
|
34
|
+
const toolMap = new Map();
|
|
35
|
+
const resources = [];
|
|
36
|
+
const resourceTemplates = [];
|
|
37
|
+
const prompts = [];
|
|
38
|
+
const promptMap = new Map();
|
|
39
|
+
const completionsMap = new Map();
|
|
40
|
+
const notificationsMailbox = yield* Mailbox.make();
|
|
41
|
+
const listChangedHandles = new Map();
|
|
42
|
+
const notifications = yield* RpcClient.makeNoSerialization(ServerNotificationRpcs, {
|
|
43
|
+
onFromClient(options) {
|
|
44
|
+
const message = options.message;
|
|
45
|
+
if (message._tag !== "Request") {
|
|
46
|
+
return Effect.void;
|
|
47
|
+
}
|
|
48
|
+
if (message.tag.includes("list_changed")) {
|
|
49
|
+
if (!listChangedHandles.has(message.tag)) {
|
|
50
|
+
listChangedHandles.set(message.tag, setTimeout(() => {
|
|
51
|
+
notificationsMailbox.unsafeOffer(message);
|
|
52
|
+
listChangedHandles.delete(message.tag);
|
|
53
|
+
}, 0));
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
notificationsMailbox.unsafeOffer(message);
|
|
57
|
+
}
|
|
58
|
+
return notifications.write({
|
|
59
|
+
clientId: 0,
|
|
60
|
+
requestId: message.id,
|
|
61
|
+
_tag: "Exit",
|
|
62
|
+
exit: Exit.void
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return McpServer.of({
|
|
67
|
+
notifications: notifications.client,
|
|
68
|
+
notificationsMailbox,
|
|
69
|
+
get tools() {
|
|
70
|
+
return tools;
|
|
71
|
+
},
|
|
72
|
+
addTool: options => Effect.suspend(() => {
|
|
73
|
+
tools.push(options.tool);
|
|
74
|
+
toolMap.set(options.tool.name, options.handle);
|
|
75
|
+
return notifications.client["notifications/tools/list_changed"]({});
|
|
76
|
+
}),
|
|
77
|
+
callTool: request => Effect.suspend(() => {
|
|
78
|
+
const handle = toolMap.get(request.name);
|
|
79
|
+
if (!handle) {
|
|
80
|
+
return Effect.fail(new InvalidParams({
|
|
81
|
+
message: `Tool '${request.name}' not found`
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
return handle(request.arguments);
|
|
85
|
+
}),
|
|
86
|
+
get resources() {
|
|
87
|
+
return resources;
|
|
88
|
+
},
|
|
89
|
+
get resourceTemplates() {
|
|
90
|
+
return resourceTemplates;
|
|
91
|
+
},
|
|
92
|
+
addResource: (resource, effect) => Effect.suspend(() => {
|
|
93
|
+
resources.push(resource);
|
|
94
|
+
matcher.add(resource.uri, {
|
|
95
|
+
_tag: "Resource",
|
|
96
|
+
effect
|
|
97
|
+
});
|
|
98
|
+
return notifications.client["notifications/resources/list_changed"]({});
|
|
99
|
+
}),
|
|
100
|
+
addResourceTemplate: ({
|
|
101
|
+
completions,
|
|
102
|
+
handle,
|
|
103
|
+
routerPath,
|
|
104
|
+
template
|
|
105
|
+
}) => Effect.suspend(() => {
|
|
106
|
+
resourceTemplates.push(template);
|
|
107
|
+
matcher.add(routerPath, {
|
|
108
|
+
_tag: "ResourceTemplate",
|
|
109
|
+
handle
|
|
110
|
+
});
|
|
111
|
+
for (const [param, handle] of Object.entries(completions)) {
|
|
112
|
+
completionsMap.set(`ref/resource/${template.uriTemplate}/${param}`, handle);
|
|
113
|
+
}
|
|
114
|
+
return notifications.client["notifications/resources/list_changed"]({});
|
|
115
|
+
}),
|
|
116
|
+
findResource: uri => Effect.suspend(() => {
|
|
117
|
+
const match = matcher.find(uri);
|
|
118
|
+
if (!match) {
|
|
119
|
+
return Effect.succeed(new ReadResourceResult({
|
|
120
|
+
contents: []
|
|
121
|
+
}));
|
|
122
|
+
} else if (match.handler._tag === "Resource") {
|
|
123
|
+
return match.handler.effect;
|
|
124
|
+
}
|
|
125
|
+
const params = [];
|
|
126
|
+
for (const key of Object.keys(match.params)) {
|
|
127
|
+
params[Number(key)] = match.params[key];
|
|
128
|
+
}
|
|
129
|
+
return match.handler.handle(uri, params);
|
|
130
|
+
}),
|
|
131
|
+
get prompts() {
|
|
132
|
+
return prompts;
|
|
133
|
+
},
|
|
134
|
+
addPrompt: options => Effect.suspend(() => {
|
|
135
|
+
prompts.push(options.prompt);
|
|
136
|
+
promptMap.set(options.prompt.name, options.handle);
|
|
137
|
+
for (const [param, handle] of Object.entries(options.completions)) {
|
|
138
|
+
completionsMap.set(`ref/prompt/${options.prompt.name}/${param}`, handle);
|
|
139
|
+
}
|
|
140
|
+
return notifications.client["notifications/prompts/list_changed"]({});
|
|
141
|
+
}),
|
|
142
|
+
getPromptResult: Effect.fnUntraced(function* ({
|
|
143
|
+
arguments: params,
|
|
144
|
+
name
|
|
145
|
+
}) {
|
|
146
|
+
const handler = promptMap.get(name);
|
|
147
|
+
if (!handler) {
|
|
148
|
+
return yield* new InvalidParams({
|
|
149
|
+
message: `Prompt '${name}' not found`
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return yield* handler(params ?? {});
|
|
153
|
+
}),
|
|
154
|
+
completion: Effect.fnUntraced(function* (complete) {
|
|
155
|
+
const ref = complete.ref;
|
|
156
|
+
const key = ref.type === "ref/resource" ? `ref/resource/${ref.uri}/${complete.argument.name}` : `ref/prompt/${ref.name}/${complete.argument.name}`;
|
|
157
|
+
const handler = completionsMap.get(key);
|
|
158
|
+
return handler ? yield* handler(complete.argument.value) : CompleteResult.empty;
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* @since 1.0.0
|
|
164
|
+
*/
|
|
165
|
+
static layer = /*#__PURE__*/Layer.scoped(McpServer, McpServer.make);
|
|
166
|
+
}
|
|
167
|
+
const LATEST_PROTOCOL_VERSION = "2025-03-26";
|
|
168
|
+
const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2024-11-05", "2024-10-07"];
|
|
169
|
+
/**
|
|
170
|
+
* @since 1.0.0
|
|
171
|
+
* @category Constructors
|
|
172
|
+
*/
|
|
173
|
+
export const run = /*#__PURE__*/Effect.fnUntraced(function* (options) {
|
|
174
|
+
const protocol = yield* RpcServer.Protocol;
|
|
175
|
+
const handlers = yield* Layer.build(layerHandlers(options));
|
|
176
|
+
const server = yield* McpServer;
|
|
177
|
+
const patchedProtocol = RpcServer.Protocol.of({
|
|
178
|
+
...protocol,
|
|
179
|
+
run: f => protocol.run((clientId, request) => {
|
|
180
|
+
if (request._tag === "Request" && request.tag.startsWith("notifications/")) {
|
|
181
|
+
if (request.tag === "notifications/cancelled") {
|
|
182
|
+
return f(clientId, {
|
|
183
|
+
_tag: "Interrupt",
|
|
184
|
+
requestId: String(request.payload.requestId)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const handler = handlers.unsafeMap.get(request.tag);
|
|
188
|
+
return handler ? handler.handler(request.payload, Headers.fromInput(request.headers)) : Effect.void;
|
|
189
|
+
}
|
|
190
|
+
return f(clientId, request);
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
const encodeNotification = Schema.encode(Schema.Union(...Array.from(ServerNotificationRpcs.requests.values(), rpc => rpc.payloadSchema)));
|
|
194
|
+
yield* server.notificationsMailbox.take.pipe(Effect.flatMap(Effect.fnUntraced(function* (request) {
|
|
195
|
+
const encoded = yield* encodeNotification(request.payload);
|
|
196
|
+
const message = {
|
|
197
|
+
_tag: "Request",
|
|
198
|
+
tag: request.tag,
|
|
199
|
+
payload: encoded
|
|
200
|
+
};
|
|
201
|
+
const clientIds = yield* patchedProtocol.clientIds;
|
|
202
|
+
for (const clientId of clientIds) {
|
|
203
|
+
yield* patchedProtocol.send(clientId, message);
|
|
204
|
+
}
|
|
205
|
+
})), Effect.catchAllCause(() => Effect.void), Effect.forever, Effect.forkScoped);
|
|
206
|
+
return yield* RpcServer.make(ClientRpcs, {
|
|
207
|
+
spanPrefix: "McpServer",
|
|
208
|
+
disableFatalDefects: true
|
|
209
|
+
}).pipe(Effect.provideService(RpcServer.Protocol, patchedProtocol), Effect.provide(handlers));
|
|
210
|
+
}, Effect.scoped);
|
|
211
|
+
/**
|
|
212
|
+
* @since 1.0.0
|
|
213
|
+
* @category Layers
|
|
214
|
+
*/
|
|
215
|
+
export const layer = options => Layer.scopedDiscard(Effect.forkScoped(run(options))).pipe(Layer.provideMerge(McpServer.layer));
|
|
216
|
+
/**
|
|
217
|
+
* Run the McpServer, using stdio for input and output.
|
|
218
|
+
*
|
|
219
|
+
* ```ts
|
|
220
|
+
* import { McpSchema, McpServer } from "@effect/ai"
|
|
221
|
+
* import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node"
|
|
222
|
+
* import { Effect, Layer, Logger, Schema } from "effect"
|
|
223
|
+
*
|
|
224
|
+
* const idParam = McpSchema.param("id", Schema.NumberFromString)
|
|
225
|
+
*
|
|
226
|
+
* // Define a resource template for a README file
|
|
227
|
+
* const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({
|
|
228
|
+
* name: "README Template",
|
|
229
|
+
* // You can add auto-completion for the ID parameter
|
|
230
|
+
* completion: {
|
|
231
|
+
* id: (_) => Effect.succeed([1, 2, 3, 4, 5])
|
|
232
|
+
* },
|
|
233
|
+
* content: Effect.fn(function*(_uri, id) {
|
|
234
|
+
* return `# MCP Server Demo - ID: ${id}`
|
|
235
|
+
* })
|
|
236
|
+
* })
|
|
237
|
+
*
|
|
238
|
+
* // Define a test prompt with parameters
|
|
239
|
+
* const TestPrompt = McpServer.prompt({
|
|
240
|
+
* name: "Test Prompt",
|
|
241
|
+
* description: "A test prompt to demonstrate MCP server capabilities",
|
|
242
|
+
* parameters: Schema.Struct({
|
|
243
|
+
* flightNumber: Schema.String
|
|
244
|
+
* }),
|
|
245
|
+
* completion: {
|
|
246
|
+
* flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"])
|
|
247
|
+
* },
|
|
248
|
+
* content: ({ flightNumber }) => Effect.succeed(`Get the booking details for flight number: ${flightNumber}`)
|
|
249
|
+
* })
|
|
250
|
+
*
|
|
251
|
+
* // Merge all the resources and prompts into a single server layer
|
|
252
|
+
* const ServerLayer = Layer.mergeAll(
|
|
253
|
+
* ReadmeTemplate,
|
|
254
|
+
* TestPrompt
|
|
255
|
+
* ).pipe(
|
|
256
|
+
* // Provide the MCP server implementation
|
|
257
|
+
* Layer.provide(McpServer.layerStdio({
|
|
258
|
+
* name: "Demo Server",
|
|
259
|
+
* version: "1.0.0",
|
|
260
|
+
* stdin: NodeStream.stdin,
|
|
261
|
+
* stdout: NodeSink.stdout
|
|
262
|
+
* })),
|
|
263
|
+
* // add a stderr logger
|
|
264
|
+
* Layer.provide(Logger.add(Logger.prettyLogger({ stderr: true })))
|
|
265
|
+
* )
|
|
266
|
+
*
|
|
267
|
+
* Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @since 1.0.0
|
|
271
|
+
* @category Layers
|
|
272
|
+
*/
|
|
273
|
+
export const layerStdio = options => layer(options).pipe(Layer.provide(RpcServer.layerProtocolStdio({
|
|
274
|
+
stdin: options.stdin,
|
|
275
|
+
stdout: options.stdout
|
|
276
|
+
})), Layer.provide(RpcSerialization.layerNdJsonRpc()),
|
|
277
|
+
// remove stdout loggers
|
|
278
|
+
Layer.provideMerge(Logger.remove(Logger.defaultLogger)), Layer.provideMerge(Logger.remove(Logger.prettyLoggerDefault)));
|
|
279
|
+
/**
|
|
280
|
+
* Run the McpServer, using HTTP for input and output.
|
|
281
|
+
*
|
|
282
|
+
* ```ts
|
|
283
|
+
* import { McpSchema, McpServer } from "@effect/ai"
|
|
284
|
+
* import { HttpRouter } from "@effect/platform"
|
|
285
|
+
* import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
286
|
+
* import { Effect, Layer, Schema } from "effect"
|
|
287
|
+
* import { createServer } from "node:http"
|
|
288
|
+
*
|
|
289
|
+
* const idParam = McpSchema.param("id", Schema.NumberFromString)
|
|
290
|
+
*
|
|
291
|
+
* // Define a resource template for a README file
|
|
292
|
+
* const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({
|
|
293
|
+
* name: "README Template",
|
|
294
|
+
* // You can add auto-completion for the ID parameter
|
|
295
|
+
* completion: {
|
|
296
|
+
* id: (_) => Effect.succeed([1, 2, 3, 4, 5])
|
|
297
|
+
* },
|
|
298
|
+
* content: Effect.fn(function*(_uri, id) {
|
|
299
|
+
* return `# MCP Server Demo - ID: ${id}`
|
|
300
|
+
* })
|
|
301
|
+
* })
|
|
302
|
+
*
|
|
303
|
+
* // Define a test prompt with parameters
|
|
304
|
+
* const TestPrompt = McpServer.prompt({
|
|
305
|
+
* name: "Test Prompt",
|
|
306
|
+
* description: "A test prompt to demonstrate MCP server capabilities",
|
|
307
|
+
* parameters: Schema.Struct({
|
|
308
|
+
* flightNumber: Schema.String
|
|
309
|
+
* }),
|
|
310
|
+
* completion: {
|
|
311
|
+
* flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"])
|
|
312
|
+
* },
|
|
313
|
+
* content: ({ flightNumber }) => Effect.succeed(`Get the booking details for flight number: ${flightNumber}`)
|
|
314
|
+
* })
|
|
315
|
+
*
|
|
316
|
+
* // Merge all the resources and prompts into a single server layer
|
|
317
|
+
* const ServerLayer = Layer.mergeAll(
|
|
318
|
+
* ReadmeTemplate,
|
|
319
|
+
* TestPrompt,
|
|
320
|
+
* HttpRouter.Default.serve()
|
|
321
|
+
* ).pipe(
|
|
322
|
+
* // Provide the MCP server implementation
|
|
323
|
+
* Layer.provide(McpServer.layerHttp({
|
|
324
|
+
* name: "Demo Server",
|
|
325
|
+
* version: "1.0.0",
|
|
326
|
+
* path: "/mcp"
|
|
327
|
+
* })),
|
|
328
|
+
* Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
329
|
+
* )
|
|
330
|
+
*
|
|
331
|
+
* Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @since 1.0.0
|
|
335
|
+
* @category Layers
|
|
336
|
+
*/
|
|
337
|
+
export const layerHttp = options => layer(options).pipe(Layer.provide(RpcServer.layerProtocolHttp(options)), Layer.provide(RpcSerialization.layerJsonRpc()));
|
|
338
|
+
/**
|
|
339
|
+
* Register an AiToolkit with the McpServer.
|
|
340
|
+
*
|
|
341
|
+
* @since 1.0.0
|
|
342
|
+
* @category Tools
|
|
343
|
+
*/
|
|
344
|
+
export const registerToolkit = /*#__PURE__*/Effect.fnUntraced(function* (toolkit) {
|
|
345
|
+
const registry = yield* McpServer;
|
|
346
|
+
const built = yield* toolkit;
|
|
347
|
+
const context = yield* Effect.context();
|
|
348
|
+
for (const tool of built.tools) {
|
|
349
|
+
const mcpTool = new Tool({
|
|
350
|
+
name: tool.name,
|
|
351
|
+
description: tool.description,
|
|
352
|
+
inputSchema: makeJsonSchema(tool.parametersSchema.ast),
|
|
353
|
+
annotations: new ToolAnnotations({
|
|
354
|
+
...Context.getOption(tool.annotations, AiTool.Title).pipe(Option.map(title => ({
|
|
355
|
+
title
|
|
356
|
+
})), Option.getOrUndefined),
|
|
357
|
+
readOnlyHint: Context.get(tool.annotations, AiTool.Readonly),
|
|
358
|
+
destructiveHint: Context.get(tool.annotations, AiTool.Destructive),
|
|
359
|
+
idempotentHint: Context.get(tool.annotations, AiTool.Idempotent),
|
|
360
|
+
openWorldHint: Context.get(tool.annotations, AiTool.OpenWorld)
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
yield* registry.addTool({
|
|
364
|
+
tool: mcpTool,
|
|
365
|
+
handle(payload) {
|
|
366
|
+
return built.handle(tool.name, payload).pipe(Effect.provide(context), Effect.match({
|
|
367
|
+
onFailure: error => new CallToolResult({
|
|
368
|
+
isError: true,
|
|
369
|
+
content: [new TextContent({
|
|
370
|
+
text: JSON.stringify(error)
|
|
371
|
+
})]
|
|
372
|
+
}),
|
|
373
|
+
onSuccess: result => new CallToolResult({
|
|
374
|
+
isError: false,
|
|
375
|
+
content: [new TextContent({
|
|
376
|
+
text: JSON.stringify(result.encodedResult)
|
|
377
|
+
})]
|
|
378
|
+
})
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
/**
|
|
385
|
+
* Register an AiToolkit with the McpServer.
|
|
386
|
+
*
|
|
387
|
+
* @since 1.0.0
|
|
388
|
+
* @category Tools
|
|
389
|
+
*/
|
|
390
|
+
export const toolkit = toolkit => Layer.effectDiscard(registerToolkit(toolkit));
|
|
391
|
+
/**
|
|
392
|
+
* Register a resource with the McpServer.
|
|
393
|
+
*
|
|
394
|
+
* @since 1.0.0
|
|
395
|
+
* @category Resources
|
|
396
|
+
*/
|
|
397
|
+
export const registerResource = function () {
|
|
398
|
+
if (arguments.length === 1) {
|
|
399
|
+
const options = arguments[0];
|
|
400
|
+
return Effect.gen(function* () {
|
|
401
|
+
const context = yield* Effect.context();
|
|
402
|
+
const registry = yield* McpServer;
|
|
403
|
+
yield* registry.addResource(new Resource({
|
|
404
|
+
...options,
|
|
405
|
+
annotations: new Annotations(options)
|
|
406
|
+
}), options.content.pipe(Effect.provide(context), Effect.map(content => resolveResourceContent(options.uri, content)), Effect.catchAllCause(cause => {
|
|
407
|
+
const prettyError = Cause.prettyErrors(cause)[0];
|
|
408
|
+
return new InternalError({
|
|
409
|
+
message: prettyError.message
|
|
410
|
+
});
|
|
411
|
+
})));
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const {
|
|
415
|
+
params,
|
|
416
|
+
routerPath,
|
|
417
|
+
schema,
|
|
418
|
+
uriPath
|
|
419
|
+
} = compileUriTemplate(...arguments);
|
|
420
|
+
return Effect.fnUntraced(function* (options) {
|
|
421
|
+
const context = yield* Effect.context();
|
|
422
|
+
const registry = yield* McpServer;
|
|
423
|
+
const decode = Schema.decodeUnknown(schema);
|
|
424
|
+
const template = new ResourceTemplate({
|
|
425
|
+
...options,
|
|
426
|
+
uriTemplate: uriPath,
|
|
427
|
+
annotations: new Annotations(options)
|
|
428
|
+
});
|
|
429
|
+
const completions = {};
|
|
430
|
+
for (const [param, handle] of Object.entries(options.completion ?? {})) {
|
|
431
|
+
const encodeArray = Schema.encodeUnknown(Schema.Array(params[param]));
|
|
432
|
+
const handler = input => handle(input).pipe(Effect.flatMap(encodeArray), Effect.map(values => new CompleteResult({
|
|
433
|
+
completion: {
|
|
434
|
+
values: values,
|
|
435
|
+
total: values.length,
|
|
436
|
+
hasMore: false
|
|
437
|
+
}
|
|
438
|
+
})), Effect.catchAllCause(cause => {
|
|
439
|
+
const prettyError = Cause.prettyErrors(cause)[0];
|
|
440
|
+
return new InternalError({
|
|
441
|
+
message: prettyError.message
|
|
442
|
+
});
|
|
443
|
+
}), Effect.provide(context));
|
|
444
|
+
completions[param] = handler;
|
|
445
|
+
}
|
|
446
|
+
yield* registry.addResourceTemplate({
|
|
447
|
+
template,
|
|
448
|
+
routerPath,
|
|
449
|
+
completions,
|
|
450
|
+
handle: (uri, params) => decode(params).pipe(Effect.mapError(error => new InvalidParams({
|
|
451
|
+
message: error.message
|
|
452
|
+
})), Effect.flatMap(params => options.content(uri, ...params).pipe(Effect.map(content => resolveResourceContent(uri, content)), Effect.catchAllCause(cause => {
|
|
453
|
+
const prettyError = Cause.prettyErrors(cause)[0];
|
|
454
|
+
return new InternalError({
|
|
455
|
+
message: prettyError.message
|
|
456
|
+
});
|
|
457
|
+
}))), Effect.provide(context))
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
/**
|
|
462
|
+
* Register a resource with the McpServer.
|
|
463
|
+
*
|
|
464
|
+
* @since 1.0.0
|
|
465
|
+
* @category Resources
|
|
466
|
+
*/
|
|
467
|
+
export const resource = function () {
|
|
468
|
+
if (arguments.length === 1) {
|
|
469
|
+
return Layer.effectDiscard(registerResource(arguments[0]));
|
|
470
|
+
}
|
|
471
|
+
const register = registerResource(...arguments);
|
|
472
|
+
return options => Layer.effectDiscard(register(options));
|
|
473
|
+
};
|
|
474
|
+
/**
|
|
475
|
+
* Register a prompt with the McpServer.
|
|
476
|
+
*
|
|
477
|
+
* @since 1.0.0
|
|
478
|
+
* @category Resources
|
|
479
|
+
*/
|
|
480
|
+
export const registerPrompt = options => {
|
|
481
|
+
const args = Arr.empty();
|
|
482
|
+
const props = {};
|
|
483
|
+
const propSignatures = options.parameters ? AST.getPropertySignatures(options.parameters.ast) : [];
|
|
484
|
+
for (const prop of propSignatures) {
|
|
485
|
+
args.push(new PromptArgument({
|
|
486
|
+
name: prop.name,
|
|
487
|
+
description: Option.getOrUndefined(AST.getDescriptionAnnotation(prop)),
|
|
488
|
+
required: !prop.isOptional
|
|
489
|
+
}));
|
|
490
|
+
props[prop.name] = Schema.make(prop.type);
|
|
491
|
+
}
|
|
492
|
+
const prompt = new Prompt({
|
|
493
|
+
name: options.name,
|
|
494
|
+
description: options.description,
|
|
495
|
+
arguments: args
|
|
496
|
+
});
|
|
497
|
+
const decode = options.parameters ? Schema.decodeUnknown(options.parameters) : () => Effect.succeed({});
|
|
498
|
+
const completion = options.completion ?? {};
|
|
499
|
+
return Effect.gen(function* () {
|
|
500
|
+
const registry = yield* McpServer;
|
|
501
|
+
const context = yield* Effect.context();
|
|
502
|
+
const completions = {};
|
|
503
|
+
for (const [param, handle] of Object.entries(completion)) {
|
|
504
|
+
const encodeArray = Schema.encodeUnknown(Schema.Array(props[param]));
|
|
505
|
+
const handler = input => handle(input).pipe(Effect.flatMap(encodeArray), Effect.map(values => new CompleteResult({
|
|
506
|
+
completion: {
|
|
507
|
+
values: values,
|
|
508
|
+
total: values.length,
|
|
509
|
+
hasMore: false
|
|
510
|
+
}
|
|
511
|
+
})), Effect.catchAllCause(cause => {
|
|
512
|
+
const prettyError = Cause.prettyErrors(cause)[0];
|
|
513
|
+
return new InternalError({
|
|
514
|
+
message: prettyError.message
|
|
515
|
+
});
|
|
516
|
+
}), Effect.provide(context));
|
|
517
|
+
completions[param] = handler;
|
|
518
|
+
}
|
|
519
|
+
yield* registry.addPrompt({
|
|
520
|
+
prompt,
|
|
521
|
+
completions,
|
|
522
|
+
handle: params => decode(params).pipe(Effect.mapError(error => new InvalidParams({
|
|
523
|
+
message: error.message
|
|
524
|
+
})), Effect.flatMap(params => options.content(params)), Effect.map(messages => {
|
|
525
|
+
messages = typeof messages === "string" ? [new PromptMessage({
|
|
526
|
+
role: "user",
|
|
527
|
+
content: new TextContent({
|
|
528
|
+
text: messages
|
|
529
|
+
})
|
|
530
|
+
})] : messages;
|
|
531
|
+
return new GetPromptResult({
|
|
532
|
+
messages,
|
|
533
|
+
description: prompt.description
|
|
534
|
+
});
|
|
535
|
+
}), Effect.catchAllCause(cause => {
|
|
536
|
+
const prettyError = Cause.prettyErrors(cause)[0];
|
|
537
|
+
return new InternalError({
|
|
538
|
+
message: prettyError.message
|
|
539
|
+
});
|
|
540
|
+
}), Effect.provide(context))
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Register a prompt with the McpServer.
|
|
546
|
+
*
|
|
547
|
+
* @since 1.0.0
|
|
548
|
+
* @category Resources
|
|
549
|
+
*/
|
|
550
|
+
export const prompt = options => Layer.effectDiscard(registerPrompt(options));
|
|
551
|
+
// -----------------------------------------------------------------------------
|
|
552
|
+
// Internal
|
|
553
|
+
// -----------------------------------------------------------------------------
|
|
554
|
+
const makeUriMatcher = () => {
|
|
555
|
+
const router = FindMyWay.make({
|
|
556
|
+
ignoreTrailingSlash: true,
|
|
557
|
+
ignoreDuplicateSlashes: true,
|
|
558
|
+
caseSensitive: true
|
|
559
|
+
});
|
|
560
|
+
const add = (uri, value) => {
|
|
561
|
+
router.on("GET", uri, value);
|
|
562
|
+
};
|
|
563
|
+
const find = uri => router.find("GET", uri);
|
|
564
|
+
return {
|
|
565
|
+
add,
|
|
566
|
+
find
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
const compileUriTemplate = (segments, ...schemas) => {
|
|
570
|
+
let routerPath = segments[0].replace(":", "::");
|
|
571
|
+
let uriPath = segments[0];
|
|
572
|
+
const params = {};
|
|
573
|
+
let pathSchema = Schema.Tuple();
|
|
574
|
+
if (schemas.length > 0) {
|
|
575
|
+
const arr = [];
|
|
576
|
+
for (let i = 0; i < schemas.length; i++) {
|
|
577
|
+
const schema = schemas[i];
|
|
578
|
+
const key = String(i);
|
|
579
|
+
arr.push(schema);
|
|
580
|
+
routerPath += `:${key}${segments[i + 1].replace(":", "::")}`;
|
|
581
|
+
const paramName = AST.getAnnotation(ParamAnnotation)(schema.ast).pipe(Option.getOrElse(() => `param${key}`));
|
|
582
|
+
params[paramName] = schema;
|
|
583
|
+
uriPath += `{${paramName}}`;
|
|
584
|
+
}
|
|
585
|
+
pathSchema = Schema.Tuple(...arr);
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
routerPath,
|
|
589
|
+
uriPath,
|
|
590
|
+
schema: pathSchema,
|
|
591
|
+
params
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
const layerHandlers = serverInfo => ClientRpcs.toLayer(Effect.gen(function* () {
|
|
595
|
+
const server = yield* McpServer;
|
|
596
|
+
return {
|
|
597
|
+
// Requests
|
|
598
|
+
ping: () => Effect.succeed({}),
|
|
599
|
+
initialize(params) {
|
|
600
|
+
const requestedVersion = params.protocolVersion;
|
|
601
|
+
const capabilities = {
|
|
602
|
+
completions: {}
|
|
603
|
+
};
|
|
604
|
+
if (server.tools.length > 0) {
|
|
605
|
+
capabilities.tools = {
|
|
606
|
+
listChanged: true
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
if (server.resources.length > 0 || server.resourceTemplates.length > 0) {
|
|
610
|
+
capabilities.resources = {
|
|
611
|
+
listChanged: true,
|
|
612
|
+
subscribe: false
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (server.prompts.length > 0) {
|
|
616
|
+
capabilities.prompts = {
|
|
617
|
+
listChanged: true
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return Effect.succeed({
|
|
621
|
+
capabilities,
|
|
622
|
+
serverInfo,
|
|
623
|
+
protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION
|
|
624
|
+
});
|
|
625
|
+
},
|
|
626
|
+
"completion/complete": server.completion,
|
|
627
|
+
"logging/setLevel": () => InternalError.notImplemented,
|
|
628
|
+
"prompts/get": server.getPromptResult,
|
|
629
|
+
"prompts/list": () => Effect.sync(() => new ListPromptsResult({
|
|
630
|
+
prompts: server.prompts
|
|
631
|
+
})),
|
|
632
|
+
"resources/list": () => Effect.sync(() => new ListResourcesResult({
|
|
633
|
+
resources: server.resources
|
|
634
|
+
})),
|
|
635
|
+
"resources/read": ({
|
|
636
|
+
uri
|
|
637
|
+
}) => server.findResource(uri),
|
|
638
|
+
"resources/subscribe": () => InternalError.notImplemented,
|
|
639
|
+
"resources/unsubscribe": () => InternalError.notImplemented,
|
|
640
|
+
"resources/templates/list": () => Effect.sync(() => new ListResourceTemplatesResult({
|
|
641
|
+
resourceTemplates: server.resourceTemplates
|
|
642
|
+
})),
|
|
643
|
+
"tools/call": server.callTool,
|
|
644
|
+
"tools/list": () => Effect.sync(() => new ListToolsResult({
|
|
645
|
+
tools: server.tools
|
|
646
|
+
})),
|
|
647
|
+
// Notifications
|
|
648
|
+
"notifications/cancelled": _ => Effect.void,
|
|
649
|
+
"notifications/initialized": _ => Effect.void,
|
|
650
|
+
"notifications/progress": _ => Effect.void,
|
|
651
|
+
"notifications/roots/list_changed": _ => Effect.void
|
|
652
|
+
};
|
|
653
|
+
}));
|
|
654
|
+
const makeJsonSchema = ast => {
|
|
655
|
+
const props = AST.getPropertySignatures(ast);
|
|
656
|
+
if (props.length === 0) {
|
|
657
|
+
return {
|
|
658
|
+
type: "object",
|
|
659
|
+
properties: {},
|
|
660
|
+
required: [],
|
|
661
|
+
additionalProperties: false
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const $defs = {};
|
|
665
|
+
const schema = JsonSchema.fromAST(ast, {
|
|
666
|
+
definitions: $defs,
|
|
667
|
+
topLevelReferenceStrategy: "skip"
|
|
668
|
+
});
|
|
669
|
+
if (Object.keys($defs).length === 0) return schema;
|
|
670
|
+
schema.$defs = $defs;
|
|
671
|
+
return schema;
|
|
672
|
+
};
|
|
673
|
+
const resolveResourceContent = (uri, content) => {
|
|
674
|
+
if (typeof content === "string") {
|
|
675
|
+
return new ReadResourceResult({
|
|
676
|
+
contents: [new TextResourceContents({
|
|
677
|
+
uri,
|
|
678
|
+
text: content
|
|
679
|
+
})]
|
|
680
|
+
});
|
|
681
|
+
} else if (content instanceof Uint8Array) {
|
|
682
|
+
return new ReadResourceResult({
|
|
683
|
+
contents: [new BlobResourceContents({
|
|
684
|
+
uri,
|
|
685
|
+
blob: content
|
|
686
|
+
})]
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return content;
|
|
690
|
+
};
|
|
691
|
+
//# sourceMappingURL=McpServer.js.map
|