@yolk-sdk/mcp 0.0.1-canary.0
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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/client/client.d.mts +40 -0
- package/dist/client/client.d.mts.map +1 -0
- package/dist/client/client.mjs +225 -0
- package/dist/client/client.mjs.map +1 -0
- package/dist/client/config.d.mts +29 -0
- package/dist/client/config.d.mts.map +1 -0
- package/dist/client/config.mjs +13 -0
- package/dist/client/config.mjs.map +1 -0
- package/dist/client/errors.d.mts +14 -0
- package/dist/client/errors.d.mts.map +1 -0
- package/dist/client/errors.mjs +22 -0
- package/dist/client/errors.mjs.map +1 -0
- package/dist/client/index.d.mts +5 -0
- package/dist/client/index.mjs +5 -0
- package/dist/client/node.d.mts +16 -0
- package/dist/client/node.d.mts.map +1 -0
- package/dist/client/node.mjs +13 -0
- package/dist/client/node.mjs.map +1 -0
- package/dist/client/protocol.d.mts +175 -0
- package/dist/client/protocol.d.mts.map +1 -0
- package/dist/client/protocol.mjs +225 -0
- package/dist/client/protocol.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/server/errors.d.mts +11 -0
- package/dist/server/errors.d.mts.map +1 -0
- package/dist/server/errors.mjs +16 -0
- package/dist/server/errors.mjs.map +1 -0
- package/dist/server/index.d.mts +4 -0
- package/dist/server/index.mjs +4 -0
- package/dist/server/server.d.mts +22 -0
- package/dist/server/server.d.mts.map +1 -0
- package/dist/server/server.mjs +180 -0
- package/dist/server/server.mjs.map +1 -0
- package/dist/server/stdio.d.mts +8 -0
- package/dist/server/stdio.d.mts.map +1 -0
- package/dist/server/stdio.mjs +18 -0
- package/dist/server/stdio.mjs.map +1 -0
- package/package.json +77 -0
- package/src/client/README.md +24 -0
- package/src/client/client.ts +494 -0
- package/src/client/config.ts +37 -0
- package/src/client/errors.ts +20 -0
- package/src/client/index.ts +48 -0
- package/src/client/node.ts +32 -0
- package/src/client/protocol.ts +364 -0
- package/src/index.ts +2 -0
- package/src/server/README.md +22 -0
- package/src/server/errors.ts +6 -0
- package/src/server/index.ts +4 -0
- package/src/server/server.ts +331 -0
- package/src/server/stdio.ts +37 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { Duration, Effect, Option, Stream } from 'effect'
|
|
2
|
+
import { HttpClient, HttpClientRequest } from 'effect/unstable/http'
|
|
3
|
+
import { ChildProcess } from 'effect/unstable/process'
|
|
4
|
+
import type { ChildProcessSpawner } from 'effect/unstable/process'
|
|
5
|
+
import type { ToolResult } from '@yolk-sdk/agent/protocol'
|
|
6
|
+
import type {
|
|
7
|
+
McpClientInfo,
|
|
8
|
+
McpLocalServerConfig,
|
|
9
|
+
McpRemoteServerConfig,
|
|
10
|
+
McpSecurityPolicy,
|
|
11
|
+
McpServerConfig
|
|
12
|
+
} from './config.ts'
|
|
13
|
+
import { defaultMcpClientInfo, defaultMcpSecurityPolicy } from './config.ts'
|
|
14
|
+
import { McpError } from './errors.ts'
|
|
15
|
+
import {
|
|
16
|
+
decodeJsonRpcResponseFromJson,
|
|
17
|
+
decodeToolCallResult,
|
|
18
|
+
decodeToolsListResult,
|
|
19
|
+
encodeJsonRpcMessage,
|
|
20
|
+
jsonRpcErrorToMcpError,
|
|
21
|
+
makeInitializedNotification,
|
|
22
|
+
makeInitializeParams,
|
|
23
|
+
makeJsonRpcRequest,
|
|
24
|
+
mcpToolToToolDef,
|
|
25
|
+
toolCallResultToToolResult,
|
|
26
|
+
type JsonRpcNotification,
|
|
27
|
+
type JsonRpcRequest,
|
|
28
|
+
type JsonRpcResponse
|
|
29
|
+
} from './protocol.ts'
|
|
30
|
+
|
|
31
|
+
const defaultRequestTimeoutMs = 30_000
|
|
32
|
+
|
|
33
|
+
export type McpClientOptions = {
|
|
34
|
+
readonly clientInfo?: McpClientInfo
|
|
35
|
+
readonly securityPolicy?: McpSecurityPolicy
|
|
36
|
+
readonly timeoutMs?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type McpResolvedTool = {
|
|
40
|
+
readonly serverName: string
|
|
41
|
+
readonly mcpToolName: string
|
|
42
|
+
readonly def: ReturnType<typeof mcpToolToToolDef>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const unknownToMessage = (error: unknown) =>
|
|
46
|
+
error instanceof Error ? error.message : String(error)
|
|
47
|
+
|
|
48
|
+
const timeoutMs = (options?: McpClientOptions) => options?.timeoutMs ?? defaultRequestTimeoutMs
|
|
49
|
+
|
|
50
|
+
const clientInfo = (options?: McpClientOptions) => options?.clientInfo ?? defaultMcpClientInfo
|
|
51
|
+
|
|
52
|
+
const securityPolicy = (options?: McpClientOptions) =>
|
|
53
|
+
options?.securityPolicy ?? defaultMcpSecurityPolicy
|
|
54
|
+
|
|
55
|
+
const fail = (server: string, message: string, cause: McpError['cause']) =>
|
|
56
|
+
Effect.fail(new McpError({ server, message, cause }))
|
|
57
|
+
|
|
58
|
+
const validateRemoteUrl = (config: McpRemoteServerConfig, policy: McpSecurityPolicy) =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const url = yield* Effect.try({
|
|
61
|
+
try: () => new URL(config.url),
|
|
62
|
+
catch: error =>
|
|
63
|
+
new McpError({
|
|
64
|
+
server: config.name,
|
|
65
|
+
message: `Invalid MCP server URL: ${unknownToMessage(error)}`,
|
|
66
|
+
cause: 'validation'
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (url.protocol === 'https:') {
|
|
71
|
+
return url.toString()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
policy.allowDevHttpLocalhost &&
|
|
76
|
+
url.protocol === 'http:' &&
|
|
77
|
+
(url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]')
|
|
78
|
+
) {
|
|
79
|
+
return url.toString()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return yield* fail(config.name, 'Remote MCP requires https: URL', 'security')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const validateLocal = (config: McpLocalServerConfig, policy: McpSecurityPolicy) =>
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
if (!policy.allowLocalServers) {
|
|
88
|
+
return yield* fail(config.name, 'Local MCP servers are disabled by policy', 'security')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (config.command.length === 0) {
|
|
92
|
+
return yield* fail(config.name, 'Local MCP command must not be empty', 'validation')
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const parseSseJsonRpcResponse = (server: string, body: string) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const candidates = [
|
|
99
|
+
body,
|
|
100
|
+
...body
|
|
101
|
+
.split('\n')
|
|
102
|
+
.filter(line => line.startsWith('data: '))
|
|
103
|
+
.map(line => line.substring('data: '.length))
|
|
104
|
+
]
|
|
105
|
+
const parsed = yield* Effect.forEach(candidates, candidate =>
|
|
106
|
+
decodeJsonRpcResponseFromJson(server, candidate).pipe(Effect.option)
|
|
107
|
+
).pipe(Effect.map(Option.firstSomeOf))
|
|
108
|
+
|
|
109
|
+
if (Option.isSome(parsed)) {
|
|
110
|
+
return parsed.value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return yield* fail(server, 'MCP response did not contain a JSON-RPC message', 'protocol')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const unwrapResponse = (server: string, response: JsonRpcResponse) =>
|
|
117
|
+
'error' in response
|
|
118
|
+
? Effect.fail(jsonRpcErrorToMcpError(server, response.error))
|
|
119
|
+
: Effect.succeed(response.result)
|
|
120
|
+
|
|
121
|
+
const mapUnknownToMcpError =
|
|
122
|
+
(server: string, message: string, cause: McpError['cause']) => (error: unknown) =>
|
|
123
|
+
error instanceof McpError
|
|
124
|
+
? error
|
|
125
|
+
: new McpError({
|
|
126
|
+
server,
|
|
127
|
+
message: `${message}: ${unknownToMessage(error)}`,
|
|
128
|
+
cause
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const findDuplicateToolName = (tools: ReadonlyArray<McpResolvedTool>) => {
|
|
132
|
+
const names = tools.map(tool => tool.def.name)
|
|
133
|
+
return Option.fromNullishOr(names.find((name, index) => names.indexOf(name) !== index))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const requestRemote = (
|
|
137
|
+
config: McpRemoteServerConfig,
|
|
138
|
+
request: JsonRpcRequest,
|
|
139
|
+
options?: McpClientOptions
|
|
140
|
+
) =>
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
const policy = securityPolicy(options)
|
|
143
|
+
const url = yield* validateRemoteUrl(config, policy)
|
|
144
|
+
const http = yield* HttpClient.HttpClient
|
|
145
|
+
const body = yield* encodeJsonRpcMessage(config.name, request)
|
|
146
|
+
const encoded = HttpClientRequest.post(url).pipe(
|
|
147
|
+
HttpClientRequest.accept('application/json, text/event-stream'),
|
|
148
|
+
HttpClientRequest.setHeaders(config.headers ?? {}),
|
|
149
|
+
HttpClientRequest.bodyText(body, 'application/json')
|
|
150
|
+
)
|
|
151
|
+
const response = yield* HttpClient.filterStatusOk(http)
|
|
152
|
+
.execute(encoded)
|
|
153
|
+
.pipe(
|
|
154
|
+
Effect.mapError(
|
|
155
|
+
error =>
|
|
156
|
+
new McpError({
|
|
157
|
+
server: config.name,
|
|
158
|
+
message: `MCP request failed: ${unknownToMessage(error)}`,
|
|
159
|
+
cause: 'transport'
|
|
160
|
+
})
|
|
161
|
+
),
|
|
162
|
+
Effect.timeoutOrElse({
|
|
163
|
+
duration: Duration.millis(timeoutMs(options)),
|
|
164
|
+
orElse: () => fail(config.name, 'MCP request timed out', 'timeout')
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
const text = yield* response.text.pipe(
|
|
168
|
+
Effect.mapError(
|
|
169
|
+
error =>
|
|
170
|
+
new McpError({
|
|
171
|
+
server: config.name,
|
|
172
|
+
message: `Could not read MCP response: ${unknownToMessage(error)}`,
|
|
173
|
+
cause: 'transport'
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
const decoded = yield* parseSseJsonRpcResponse(config.name, text)
|
|
178
|
+
|
|
179
|
+
return yield* unwrapResponse(config.name, decoded)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const notifyRemote = (
|
|
183
|
+
config: McpRemoteServerConfig,
|
|
184
|
+
notification: JsonRpcNotification,
|
|
185
|
+
options?: McpClientOptions
|
|
186
|
+
) =>
|
|
187
|
+
Effect.gen(function* () {
|
|
188
|
+
const policy = securityPolicy(options)
|
|
189
|
+
const url = yield* validateRemoteUrl(config, policy)
|
|
190
|
+
const http = yield* HttpClient.HttpClient
|
|
191
|
+
const body = yield* encodeJsonRpcMessage(config.name, notification)
|
|
192
|
+
const encoded = HttpClientRequest.post(url).pipe(
|
|
193
|
+
HttpClientRequest.accept('application/json, text/event-stream'),
|
|
194
|
+
HttpClientRequest.setHeaders(config.headers ?? {}),
|
|
195
|
+
HttpClientRequest.bodyText(body, 'application/json')
|
|
196
|
+
)
|
|
197
|
+
yield* HttpClient.filterStatusOk(http)
|
|
198
|
+
.execute(encoded)
|
|
199
|
+
.pipe(
|
|
200
|
+
Effect.mapError(
|
|
201
|
+
error =>
|
|
202
|
+
new McpError({
|
|
203
|
+
server: config.name,
|
|
204
|
+
message: `MCP notification failed: ${unknownToMessage(error)}`,
|
|
205
|
+
cause: 'transport'
|
|
206
|
+
})
|
|
207
|
+
),
|
|
208
|
+
Effect.timeoutOrElse({
|
|
209
|
+
duration: Duration.millis(timeoutMs(options)),
|
|
210
|
+
orElse: () => fail(config.name, 'MCP notification timed out', 'timeout')
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const requestRemoteSession = (
|
|
216
|
+
config: McpRemoteServerConfig,
|
|
217
|
+
request: JsonRpcRequest,
|
|
218
|
+
options?: McpClientOptions
|
|
219
|
+
) =>
|
|
220
|
+
Effect.gen(function* () {
|
|
221
|
+
yield* requestRemote(config, initializeRequest(options), options)
|
|
222
|
+
yield* notifyRemote(config, makeInitializedNotification(), options)
|
|
223
|
+
return yield* requestRemote(config, request, options)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
type EncodedLocalMessage = {
|
|
227
|
+
readonly message: JsonRpcRequest | JsonRpcNotification
|
|
228
|
+
readonly line: string
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const requestLocalEncoded = (
|
|
232
|
+
config: McpLocalServerConfig,
|
|
233
|
+
messages: ReadonlyArray<EncodedLocalMessage>,
|
|
234
|
+
expectedResponses: number,
|
|
235
|
+
options?: McpClientOptions
|
|
236
|
+
) => {
|
|
237
|
+
const policy = securityPolicy(options)
|
|
238
|
+
if (!policy.allowLocalServers) {
|
|
239
|
+
return fail(config.name, 'Local MCP servers are disabled by policy', 'security')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const command = config.command[0]
|
|
243
|
+
if (command === undefined) {
|
|
244
|
+
return fail(config.name, 'Local MCP command must not be empty', 'validation')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return Effect.gen(function* () {
|
|
248
|
+
const stdin = Stream.fromIterable(messages.map(message => `${message.line}\n`)).pipe(
|
|
249
|
+
Stream.encodeText
|
|
250
|
+
)
|
|
251
|
+
const child = yield* ChildProcess.make(command, config.command.slice(1), {
|
|
252
|
+
env: config.environment ?? {},
|
|
253
|
+
extendEnv: false,
|
|
254
|
+
stdin: { stream: stdin, endOnDone: true },
|
|
255
|
+
stderr: 'ignore'
|
|
256
|
+
})
|
|
257
|
+
const lines = yield* child.stdout.pipe(
|
|
258
|
+
Stream.decodeText,
|
|
259
|
+
Stream.splitLines,
|
|
260
|
+
Stream.filter(line => line.length > 0),
|
|
261
|
+
Stream.take(expectedResponses),
|
|
262
|
+
Stream.runCollect
|
|
263
|
+
)
|
|
264
|
+
const responses = yield* Effect.forEach(lines, line =>
|
|
265
|
+
decodeJsonRpcResponseFromJson(config.name, line)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if (responses.length < expectedResponses) {
|
|
269
|
+
return yield* fail(config.name, 'Local MCP did not return expected response', 'protocol')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return responses
|
|
273
|
+
}).pipe(
|
|
274
|
+
Effect.scoped,
|
|
275
|
+
Effect.timeoutOrElse({
|
|
276
|
+
duration: Duration.millis(timeoutMs(options)),
|
|
277
|
+
orElse: () => fail(config.name, 'Local MCP request timed out', 'timeout')
|
|
278
|
+
}),
|
|
279
|
+
Effect.mapError(mapUnknownToMcpError(config.name, 'Local MCP request failed', 'transport'))
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const requestLocal = (
|
|
284
|
+
config: McpLocalServerConfig,
|
|
285
|
+
requests: ReadonlyArray<JsonRpcRequest | JsonRpcNotification>,
|
|
286
|
+
expectedResponses: number,
|
|
287
|
+
options?: McpClientOptions
|
|
288
|
+
) =>
|
|
289
|
+
Effect.gen(function* () {
|
|
290
|
+
const messages = yield* Effect.forEach(requests, request =>
|
|
291
|
+
encodeJsonRpcMessage(config.name, request).pipe(
|
|
292
|
+
Effect.map(line => ({ message: request, line }))
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return yield* requestLocalEncoded(config, messages, expectedResponses, options)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const initializeRequest = (options?: McpClientOptions) =>
|
|
300
|
+
makeJsonRpcRequest({
|
|
301
|
+
id: 1,
|
|
302
|
+
method: 'initialize',
|
|
303
|
+
params: makeInitializeParams(clientInfo(options))
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const listToolsRequest = () => makeJsonRpcRequest({ id: 2, method: 'tools/list' })
|
|
307
|
+
|
|
308
|
+
const callToolRequest = (input: { readonly toolName: string; readonly params: unknown }) =>
|
|
309
|
+
makeJsonRpcRequest({
|
|
310
|
+
id: 3,
|
|
311
|
+
method: 'tools/call',
|
|
312
|
+
params: { name: input.toolName, arguments: input.params }
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const responseById = (responses: ReadonlyArray<JsonRpcResponse>, id: string | number) =>
|
|
316
|
+
Option.fromNullishOr(responses.find(response => response.id === id))
|
|
317
|
+
|
|
318
|
+
const requestLocalSession = (
|
|
319
|
+
config: McpLocalServerConfig,
|
|
320
|
+
request: JsonRpcRequest,
|
|
321
|
+
options?: McpClientOptions
|
|
322
|
+
) =>
|
|
323
|
+
Effect.gen(function* () {
|
|
324
|
+
const initialize = initializeRequest(options)
|
|
325
|
+
const responses = yield* requestLocal(
|
|
326
|
+
config,
|
|
327
|
+
[initialize, makeInitializedNotification(), request],
|
|
328
|
+
2,
|
|
329
|
+
options
|
|
330
|
+
)
|
|
331
|
+
const initializeResponse = responseById(responses, initialize.id)
|
|
332
|
+
if (Option.isNone(initializeResponse)) {
|
|
333
|
+
return yield* fail(config.name, 'Local MCP did not return initialize response', 'protocol')
|
|
334
|
+
}
|
|
335
|
+
yield* unwrapResponse(config.name, initializeResponse.value)
|
|
336
|
+
|
|
337
|
+
return responses
|
|
338
|
+
}).pipe(
|
|
339
|
+
Effect.flatMap(responses => {
|
|
340
|
+
const response = responseById(responses, request.id)
|
|
341
|
+
return Option.isNone(response)
|
|
342
|
+
? fail(config.name, 'Local MCP did not return expected response', 'protocol')
|
|
343
|
+
: unwrapResponse(config.name, response.value)
|
|
344
|
+
})
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const resolveMcpTools = (config: McpServerConfig, result: unknown) =>
|
|
348
|
+
Effect.gen(function* () {
|
|
349
|
+
const tools = yield* decodeToolsListResult(result).pipe(
|
|
350
|
+
Effect.mapError(
|
|
351
|
+
error =>
|
|
352
|
+
new McpError({
|
|
353
|
+
server: config.name,
|
|
354
|
+
message: `Invalid tools/list result: ${unknownToMessage(error)}`,
|
|
355
|
+
cause: 'validation'
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return tools.tools.map(tool => ({
|
|
361
|
+
serverName: config.name,
|
|
362
|
+
mcpToolName: tool.name,
|
|
363
|
+
def: mcpToolToToolDef({ serverName: config.name, tool })
|
|
364
|
+
}))
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
export const listRemoteMcpServerTools = (
|
|
368
|
+
config: McpRemoteServerConfig,
|
|
369
|
+
options?: McpClientOptions
|
|
370
|
+
): Effect.Effect<ReadonlyArray<McpResolvedTool>, McpError, HttpClient.HttpClient> =>
|
|
371
|
+
Effect.gen(function* () {
|
|
372
|
+
if (config.enabled === false) {
|
|
373
|
+
return []
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const result = yield* requestRemoteSession(config, listToolsRequest(), options)
|
|
377
|
+
return yield* resolveMcpTools(config, result)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
export const listLocalMcpServerTools = (
|
|
381
|
+
config: McpLocalServerConfig,
|
|
382
|
+
options?: McpClientOptions
|
|
383
|
+
): Effect.Effect<
|
|
384
|
+
ReadonlyArray<McpResolvedTool>,
|
|
385
|
+
McpError,
|
|
386
|
+
ChildProcessSpawner.ChildProcessSpawner
|
|
387
|
+
> =>
|
|
388
|
+
Effect.gen(function* () {
|
|
389
|
+
if (config.enabled === false) {
|
|
390
|
+
return []
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
yield* validateLocal(config, securityPolicy(options))
|
|
394
|
+
const result = yield* requestLocalSession(config, listToolsRequest(), options)
|
|
395
|
+
return yield* resolveMcpTools(config, result)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
export const listMcpServerTools = (config: McpServerConfig, options?: McpClientOptions) =>
|
|
399
|
+
Effect.gen(function* () {
|
|
400
|
+
if (config.enabled === false) {
|
|
401
|
+
return []
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (config.type === 'local') {
|
|
405
|
+
return yield* listLocalMcpServerTools(config, options)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return yield* listRemoteMcpServerTools(config, options)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
export type CallMcpServerToolInput = {
|
|
412
|
+
readonly config: McpServerConfig
|
|
413
|
+
readonly mcpToolName: string
|
|
414
|
+
readonly toolCallId: string
|
|
415
|
+
readonly params: unknown
|
|
416
|
+
readonly options?: McpClientOptions
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const resolveMcpToolResult = (input: CallMcpServerToolInput, result: unknown) =>
|
|
420
|
+
Effect.gen(function* () {
|
|
421
|
+
const toolCallResult = yield* decodeToolCallResult(result).pipe(
|
|
422
|
+
Effect.mapError(
|
|
423
|
+
error =>
|
|
424
|
+
new McpError({
|
|
425
|
+
server: input.config.name,
|
|
426
|
+
message: `Invalid tools/call result: ${unknownToMessage(error)}`,
|
|
427
|
+
cause: 'validation'
|
|
428
|
+
})
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
return toolCallResultToToolResult({ toolCallId: input.toolCallId, result: toolCallResult })
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
export const callRemoteMcpServerTool = (
|
|
436
|
+
input: Omit<CallMcpServerToolInput, 'config'> & { readonly config: McpRemoteServerConfig }
|
|
437
|
+
): Effect.Effect<ToolResult, McpError, HttpClient.HttpClient> =>
|
|
438
|
+
Effect.gen(function* () {
|
|
439
|
+
const result = yield* requestRemoteSession(
|
|
440
|
+
input.config,
|
|
441
|
+
callToolRequest({ toolName: input.mcpToolName, params: input.params }),
|
|
442
|
+
input.options
|
|
443
|
+
)
|
|
444
|
+
return yield* resolveMcpToolResult(input, result)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
export const callLocalMcpServerTool = (
|
|
448
|
+
input: Omit<CallMcpServerToolInput, 'config'> & { readonly config: McpLocalServerConfig }
|
|
449
|
+
): Effect.Effect<ToolResult, McpError, ChildProcessSpawner.ChildProcessSpawner> =>
|
|
450
|
+
Effect.gen(function* () {
|
|
451
|
+
const result = yield* requestLocalSession(
|
|
452
|
+
input.config,
|
|
453
|
+
callToolRequest({ toolName: input.mcpToolName, params: input.params }),
|
|
454
|
+
input.options
|
|
455
|
+
)
|
|
456
|
+
return yield* resolveMcpToolResult(input, result)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
export const callMcpServerTool = (
|
|
460
|
+
input: CallMcpServerToolInput
|
|
461
|
+
): Effect.Effect<
|
|
462
|
+
ToolResult,
|
|
463
|
+
McpError,
|
|
464
|
+
ChildProcessSpawner.ChildProcessSpawner | HttpClient.HttpClient
|
|
465
|
+
> =>
|
|
466
|
+
input.config.type === 'local'
|
|
467
|
+
? callLocalMcpServerTool({
|
|
468
|
+
config: input.config,
|
|
469
|
+
mcpToolName: input.mcpToolName,
|
|
470
|
+
toolCallId: input.toolCallId,
|
|
471
|
+
params: input.params,
|
|
472
|
+
options: input.options
|
|
473
|
+
})
|
|
474
|
+
: callRemoteMcpServerTool({
|
|
475
|
+
config: input.config,
|
|
476
|
+
mcpToolName: input.mcpToolName,
|
|
477
|
+
toolCallId: input.toolCallId,
|
|
478
|
+
params: input.params,
|
|
479
|
+
options: input.options
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
export const listMcpTools = (configs: ReadonlyArray<McpServerConfig>, options?: McpClientOptions) =>
|
|
483
|
+
Effect.flatMap(
|
|
484
|
+
Effect.forEach(configs, config => listMcpServerTools(config, options)),
|
|
485
|
+
tools => {
|
|
486
|
+
const resolved = tools.flat()
|
|
487
|
+
const duplicate = findDuplicateToolName(resolved)
|
|
488
|
+
if (Option.isSome(duplicate)) {
|
|
489
|
+
return fail('mcp', `Duplicate MCP tool name: ${duplicate.value}`, 'validation')
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return Effect.succeed(resolved)
|
|
493
|
+
}
|
|
494
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type McpRemoteServerConfig = {
|
|
2
|
+
readonly name: string
|
|
3
|
+
readonly type: 'remote'
|
|
4
|
+
readonly url: string
|
|
5
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
6
|
+
readonly enabled?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type McpLocalServerConfig = {
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly type: 'local'
|
|
12
|
+
readonly command: ReadonlyArray<string>
|
|
13
|
+
readonly environment?: Readonly<Record<string, string>>
|
|
14
|
+
readonly enabled?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type McpServerConfig = McpRemoteServerConfig | McpLocalServerConfig
|
|
18
|
+
|
|
19
|
+
export type McpClientInfo = {
|
|
20
|
+
readonly name: string
|
|
21
|
+
readonly version: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type McpSecurityPolicy = {
|
|
25
|
+
readonly allowLocalServers: boolean
|
|
26
|
+
readonly allowDevHttpLocalhost: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const defaultMcpClientInfo: McpClientInfo = {
|
|
30
|
+
name: 'yolk',
|
|
31
|
+
version: '0.1.0'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const defaultMcpSecurityPolicy: McpSecurityPolicy = {
|
|
35
|
+
allowLocalServers: false,
|
|
36
|
+
allowDevHttpLocalhost: false
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Schema from 'effect/Schema'
|
|
2
|
+
|
|
3
|
+
export const McpErrorCause = Schema.Literals([
|
|
4
|
+
'disabled',
|
|
5
|
+
'security',
|
|
6
|
+
'transport',
|
|
7
|
+
'protocol',
|
|
8
|
+
'parse',
|
|
9
|
+
'encoding',
|
|
10
|
+
'timeout',
|
|
11
|
+
'validation',
|
|
12
|
+
'tool_error'
|
|
13
|
+
])
|
|
14
|
+
export type McpErrorCause = typeof McpErrorCause.Type
|
|
15
|
+
|
|
16
|
+
export class McpError extends Schema.TaggedErrorClass<McpError>()('McpError', {
|
|
17
|
+
server: Schema.String,
|
|
18
|
+
message: Schema.String,
|
|
19
|
+
cause: McpErrorCause
|
|
20
|
+
}) {}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export { defaultMcpClientInfo, defaultMcpSecurityPolicy } from './config.ts'
|
|
2
|
+
export type {
|
|
3
|
+
McpClientInfo,
|
|
4
|
+
McpLocalServerConfig,
|
|
5
|
+
McpRemoteServerConfig,
|
|
6
|
+
McpSecurityPolicy,
|
|
7
|
+
McpServerConfig
|
|
8
|
+
} from './config.ts'
|
|
9
|
+
export {
|
|
10
|
+
callLocalMcpServerTool,
|
|
11
|
+
callMcpServerTool,
|
|
12
|
+
callRemoteMcpServerTool,
|
|
13
|
+
listLocalMcpServerTools,
|
|
14
|
+
listMcpServerTools,
|
|
15
|
+
listMcpTools,
|
|
16
|
+
listRemoteMcpServerTools
|
|
17
|
+
} from './client.ts'
|
|
18
|
+
export type { CallMcpServerToolInput, McpClientOptions, McpResolvedTool } from './client.ts'
|
|
19
|
+
export { McpError, McpErrorCause } from './errors.ts'
|
|
20
|
+
export {
|
|
21
|
+
decodeJsonRpcMessageFromJson,
|
|
22
|
+
decodeJsonRpcResponse,
|
|
23
|
+
decodeJsonRpcResponseFromJson,
|
|
24
|
+
decodeToolCallResult,
|
|
25
|
+
decodeToolsListResult,
|
|
26
|
+
encodeJsonRpcMessage,
|
|
27
|
+
GenericContentBlock,
|
|
28
|
+
JsonRpcErrorObject,
|
|
29
|
+
JsonRpcErrorResponse,
|
|
30
|
+
JsonRpcMessage,
|
|
31
|
+
JsonRpcNotification,
|
|
32
|
+
JsonRpcRequest,
|
|
33
|
+
JsonRpcResponse,
|
|
34
|
+
JsonRpcSuccessResponse,
|
|
35
|
+
jsonRpcErrorToMcpError,
|
|
36
|
+
latestMcpProtocolVersion,
|
|
37
|
+
makeInitializedNotification,
|
|
38
|
+
makeInitializeParams,
|
|
39
|
+
makeJsonRpcRequest,
|
|
40
|
+
McpTool,
|
|
41
|
+
mcpToolToToolDef,
|
|
42
|
+
sanitizeMcpName,
|
|
43
|
+
TextContentBlock,
|
|
44
|
+
ToolCallResult,
|
|
45
|
+
toolCallResultToToolResult,
|
|
46
|
+
ToolsListResult
|
|
47
|
+
} from './protocol.ts'
|
|
48
|
+
export type { JsonRpcMessage as JsonRpcMessageType } from './protocol.ts'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import { NodeServices } from '@effect/platform-node'
|
|
3
|
+
import type { McpLocalServerConfig, McpServerConfig } from './config.ts'
|
|
4
|
+
import {
|
|
5
|
+
callLocalMcpServerTool,
|
|
6
|
+
callMcpServerTool,
|
|
7
|
+
listLocalMcpServerTools,
|
|
8
|
+
listMcpServerTools,
|
|
9
|
+
listMcpTools,
|
|
10
|
+
type CallMcpServerToolInput,
|
|
11
|
+
type McpClientOptions
|
|
12
|
+
} from './client.ts'
|
|
13
|
+
|
|
14
|
+
export const listLocalMcpServerToolsNode = (
|
|
15
|
+
config: McpLocalServerConfig,
|
|
16
|
+
options?: McpClientOptions
|
|
17
|
+
) => listLocalMcpServerTools(config, options).pipe(Effect.provide(NodeServices.layer))
|
|
18
|
+
|
|
19
|
+
export const listMcpServerToolsNode = (config: McpServerConfig, options?: McpClientOptions) =>
|
|
20
|
+
listMcpServerTools(config, options).pipe(Effect.provide(NodeServices.layer))
|
|
21
|
+
|
|
22
|
+
export const listMcpToolsNode = (
|
|
23
|
+
configs: ReadonlyArray<McpServerConfig>,
|
|
24
|
+
options?: McpClientOptions
|
|
25
|
+
) => listMcpTools(configs, options).pipe(Effect.provide(NodeServices.layer))
|
|
26
|
+
|
|
27
|
+
export const callLocalMcpServerToolNode = (
|
|
28
|
+
input: Omit<CallMcpServerToolInput, 'config'> & { readonly config: McpLocalServerConfig }
|
|
29
|
+
) => callLocalMcpServerTool(input).pipe(Effect.provide(NodeServices.layer))
|
|
30
|
+
|
|
31
|
+
export const callMcpServerToolNode = (input: CallMcpServerToolInput) =>
|
|
32
|
+
callMcpServerTool(input).pipe(Effect.provide(NodeServices.layer))
|