@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,331 @@
|
|
|
1
|
+
import { Array as Arr, Effect, Option } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import {
|
|
4
|
+
ToolCall,
|
|
5
|
+
contentParts,
|
|
6
|
+
type ContentPart,
|
|
7
|
+
type ToolDef,
|
|
8
|
+
type ToolResult
|
|
9
|
+
} from '@yolk-sdk/agent/protocol'
|
|
10
|
+
import { latestMcpProtocolVersion } from '@yolk-sdk/mcp/client'
|
|
11
|
+
import { McpServerError } from './errors.ts'
|
|
12
|
+
|
|
13
|
+
type JsonRpcRequest = {
|
|
14
|
+
readonly jsonrpc: '2.0'
|
|
15
|
+
readonly id: string | number
|
|
16
|
+
readonly method: string
|
|
17
|
+
readonly params?: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type JsonRpcNotification = {
|
|
21
|
+
readonly jsonrpc: '2.0'
|
|
22
|
+
readonly method: string
|
|
23
|
+
readonly params?: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type JsonRpcResponse = {
|
|
27
|
+
readonly jsonrpc: '2.0'
|
|
28
|
+
readonly id: string | number | null
|
|
29
|
+
readonly result?: unknown
|
|
30
|
+
readonly error?: {
|
|
31
|
+
readonly code: number
|
|
32
|
+
readonly message: string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const JsonRpcRequestSchema = Schema.Struct({
|
|
37
|
+
jsonrpc: Schema.Literal('2.0'),
|
|
38
|
+
id: Schema.Union([Schema.String, Schema.Number]),
|
|
39
|
+
method: Schema.String,
|
|
40
|
+
params: Schema.optional(Schema.Unknown)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const JsonRpcNotificationSchema = Schema.Struct({
|
|
44
|
+
jsonrpc: Schema.Literal('2.0'),
|
|
45
|
+
method: Schema.String,
|
|
46
|
+
params: Schema.optional(Schema.Unknown)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const CallToolParamsSchema = Schema.Struct({
|
|
50
|
+
name: Schema.String,
|
|
51
|
+
arguments: Schema.optional(Schema.Unknown)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const JsonRpcMessageSchema = Schema.Union([JsonRpcRequestSchema, JsonRpcNotificationSchema])
|
|
55
|
+
|
|
56
|
+
type JsonRpcMessage = typeof JsonRpcMessageSchema.Type
|
|
57
|
+
|
|
58
|
+
type DecodedLine =
|
|
59
|
+
| { readonly _tag: 'Message'; readonly message: JsonRpcMessage }
|
|
60
|
+
| { readonly _tag: 'Response'; readonly response: Option.Option<string> }
|
|
61
|
+
|
|
62
|
+
const decodedMessage = (message: JsonRpcMessage): DecodedLine => ({ _tag: 'Message', message })
|
|
63
|
+
|
|
64
|
+
const decodedResponse = (response: Option.Option<string>): DecodedLine => ({
|
|
65
|
+
_tag: 'Response',
|
|
66
|
+
response
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)
|
|
70
|
+
const decodeJsonRpcMessage = Schema.decodeUnknownEffect(JsonRpcMessageSchema)
|
|
71
|
+
const decodeCallToolParams = Schema.decodeUnknownEffect(CallToolParamsSchema)
|
|
72
|
+
|
|
73
|
+
const encodeJson = (value: unknown) =>
|
|
74
|
+
Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)(value).pipe(
|
|
75
|
+
Effect.mapError(
|
|
76
|
+
error =>
|
|
77
|
+
new McpServerError({
|
|
78
|
+
message: `Could not encode MCP response: ${String(error)}`,
|
|
79
|
+
cause: 'encoding'
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const decodeMessage = (line: string) =>
|
|
85
|
+
decodeJson(line).pipe(
|
|
86
|
+
Effect.mapError(
|
|
87
|
+
error =>
|
|
88
|
+
new McpServerError({
|
|
89
|
+
message: `Malformed MCP JSON: ${String(error)}`,
|
|
90
|
+
cause: 'parse'
|
|
91
|
+
})
|
|
92
|
+
),
|
|
93
|
+
Effect.flatMap(value =>
|
|
94
|
+
decodeJsonRpcMessage(value).pipe(
|
|
95
|
+
Effect.mapError(
|
|
96
|
+
error =>
|
|
97
|
+
new McpServerError({
|
|
98
|
+
message: `Invalid MCP JSON-RPC message: ${String(error)}`,
|
|
99
|
+
cause: 'validation'
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const successResponse = (id: string | number, result: unknown): JsonRpcResponse => ({
|
|
107
|
+
jsonrpc: '2.0',
|
|
108
|
+
id,
|
|
109
|
+
result
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const errorResponse = (
|
|
113
|
+
id: string | number | null,
|
|
114
|
+
code: number,
|
|
115
|
+
message: string
|
|
116
|
+
): JsonRpcResponse => ({
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id,
|
|
119
|
+
error: { code, message }
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const unknownToMessage = (error: unknown) =>
|
|
123
|
+
error instanceof Error ? error.message : String(error)
|
|
124
|
+
|
|
125
|
+
const isRequest = (message: JsonRpcRequest | JsonRpcNotification): message is JsonRpcRequest =>
|
|
126
|
+
'id' in message
|
|
127
|
+
|
|
128
|
+
const protocolErrorCode = (error: McpServerError) => {
|
|
129
|
+
switch (error.cause) {
|
|
130
|
+
case 'parse':
|
|
131
|
+
return -32_700
|
|
132
|
+
case 'validation':
|
|
133
|
+
return -32_600
|
|
134
|
+
case 'protocol':
|
|
135
|
+
return -32_603
|
|
136
|
+
case 'tool_error':
|
|
137
|
+
case 'encoding':
|
|
138
|
+
return -32_000
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const protocolErrorResponse = (id: string | number | null, error: McpServerError) =>
|
|
143
|
+
errorResponse(id, protocolErrorCode(error), error.message)
|
|
144
|
+
|
|
145
|
+
const mcpContentBlockFromPart = (part: ContentPart) => {
|
|
146
|
+
switch (part._tag) {
|
|
147
|
+
case 'Text':
|
|
148
|
+
return { type: 'text', text: part.text }
|
|
149
|
+
case 'Image':
|
|
150
|
+
return { type: 'image', data: part.data, mimeType: part.mimeType }
|
|
151
|
+
case 'Audio':
|
|
152
|
+
return { type: 'audio', data: part.data, mimeType: `audio/${part.format}` }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mcpResultFromToolResult = (result: ToolResult) => {
|
|
157
|
+
const content = Arr.map(contentParts(result.content), mcpContentBlockFromPart)
|
|
158
|
+
const base = result.isError === undefined ? { content } : { content, isError: result.isError }
|
|
159
|
+
|
|
160
|
+
return result.structuredContent === undefined
|
|
161
|
+
? base
|
|
162
|
+
: { ...base, structuredContent: result.structuredContent }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const mcpErrorResult = (message: string) => ({
|
|
166
|
+
content: [{ type: 'text', text: message }],
|
|
167
|
+
isError: true
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const mcpResultFromExecutionResult = (result: ToolResult | ReturnType<typeof mcpErrorResult>) =>
|
|
171
|
+
'toolCallId' in result ? mcpResultFromToolResult(result) : result
|
|
172
|
+
|
|
173
|
+
const toolListItem = (tool: ToolDef) => ({
|
|
174
|
+
name: tool.name,
|
|
175
|
+
description: tool.description,
|
|
176
|
+
inputSchema: tool.parameters
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
export type McpServerTool<R = never> = {
|
|
180
|
+
readonly def: ToolDef
|
|
181
|
+
readonly execute: (call: ToolCall) => Effect.Effect<ToolResult, McpServerError, R>
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type McpToolServer<R = never> = {
|
|
185
|
+
readonly handleLine: (line: string) => Effect.Effect<Option.Option<string>, McpServerError, R>
|
|
186
|
+
readonly handleJson: (body: string) => Effect.Effect<string, McpServerError, R>
|
|
187
|
+
readonly handleHttpRequest: (request: Request) => Effect.Effect<Response, never, R>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const jsonResponse = (body: string, init?: ResponseInit) =>
|
|
191
|
+
new Response(body, {
|
|
192
|
+
...init,
|
|
193
|
+
headers: {
|
|
194
|
+
'content-type': 'application/json',
|
|
195
|
+
...(init?.headers ?? {})
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const methodNotAllowedBody = () =>
|
|
200
|
+
encodeJson(errorResponse(null, -32_600, 'Method not allowed')).pipe(
|
|
201
|
+
Effect.orElseSucceed(
|
|
202
|
+
() => '{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Method not allowed"}}'
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const badRequestBody = (message: string) =>
|
|
207
|
+
encodeJson(errorResponse(null, -32_600, message)).pipe(
|
|
208
|
+
Effect.orElseSucceed(
|
|
209
|
+
() => '{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Bad request"}}'
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
export const makeMcpToolServer = <R>(input: {
|
|
214
|
+
readonly name: string
|
|
215
|
+
readonly version: string
|
|
216
|
+
readonly tools: ReadonlyArray<McpServerTool<R>>
|
|
217
|
+
}): McpToolServer<R> => {
|
|
218
|
+
const findTool = (name: string) => Arr.findFirst(input.tools, tool => tool.def.name === name)
|
|
219
|
+
|
|
220
|
+
const handleRequest = (request: JsonRpcRequest) =>
|
|
221
|
+
Effect.gen(function* () {
|
|
222
|
+
switch (request.method) {
|
|
223
|
+
case 'initialize':
|
|
224
|
+
return successResponse(request.id, {
|
|
225
|
+
protocolVersion: latestMcpProtocolVersion,
|
|
226
|
+
capabilities: { tools: {} },
|
|
227
|
+
serverInfo: { name: input.name, version: input.version }
|
|
228
|
+
})
|
|
229
|
+
case 'tools/list':
|
|
230
|
+
return successResponse(request.id, {
|
|
231
|
+
tools: input.tools.map(tool => toolListItem(tool.def))
|
|
232
|
+
})
|
|
233
|
+
case 'tools/call': {
|
|
234
|
+
const params = yield* decodeCallToolParams(request.params).pipe(
|
|
235
|
+
Effect.mapError(
|
|
236
|
+
error =>
|
|
237
|
+
new McpServerError({
|
|
238
|
+
message: `Invalid tools/call params: ${String(error)}`,
|
|
239
|
+
cause: 'validation'
|
|
240
|
+
})
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
const tool = findTool(params.name)
|
|
244
|
+
if (Option.isNone(tool)) {
|
|
245
|
+
return errorResponse(request.id, -32_602, `Unknown tool: ${params.name}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const result = yield* tool.value
|
|
249
|
+
.execute(
|
|
250
|
+
ToolCall.make({
|
|
251
|
+
id: String(request.id),
|
|
252
|
+
name: params.name,
|
|
253
|
+
params: params.arguments ?? {}
|
|
254
|
+
})
|
|
255
|
+
)
|
|
256
|
+
.pipe(
|
|
257
|
+
Effect.catch(error =>
|
|
258
|
+
Effect.succeed(mcpErrorResult(`MCP tool failed: ${error.message}`))
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return successResponse(request.id, mcpResultFromExecutionResult(result))
|
|
263
|
+
}
|
|
264
|
+
default:
|
|
265
|
+
return errorResponse(request.id, -32_601, `Method not found: ${request.method}`)
|
|
266
|
+
}
|
|
267
|
+
}).pipe(
|
|
268
|
+
Effect.catch(error =>
|
|
269
|
+
error instanceof McpServerError
|
|
270
|
+
? Effect.succeed(protocolErrorResponse(request.id, error))
|
|
271
|
+
: Effect.succeed(errorResponse(request.id, -32_000, unknownToMessage(error)))
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const handleLine = (line: string) =>
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
const decoded: DecodedLine = yield* decodeMessage(line).pipe(
|
|
278
|
+
Effect.map(decodedMessage),
|
|
279
|
+
Effect.catch(error =>
|
|
280
|
+
encodeJson(protocolErrorResponse(null, error)).pipe(
|
|
281
|
+
Effect.map(response => decodedResponse(Option.some(response)))
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (decoded._tag === 'Response') {
|
|
287
|
+
return decoded.response
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { message } = decoded
|
|
291
|
+
|
|
292
|
+
if (!isRequest(message)) {
|
|
293
|
+
return Option.none<string>()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const response = yield* handleRequest(message)
|
|
297
|
+
const encoded = yield* encodeJson(response)
|
|
298
|
+
return Option.some(encoded)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const handleJson = (body: string) =>
|
|
302
|
+
Effect.gen(function* () {
|
|
303
|
+
const response = yield* handleLine(body)
|
|
304
|
+
if (Option.isNone(response)) {
|
|
305
|
+
return yield* encodeJson(errorResponse(null, -32_600, 'Notifications have no response'))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return response.value
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const handleHttpRequest = (request: Request) =>
|
|
312
|
+
Effect.gen(function* () {
|
|
313
|
+
if (request.method !== 'POST') {
|
|
314
|
+
const body = yield* methodNotAllowedBody()
|
|
315
|
+
return jsonResponse(body, { status: 405, headers: { allow: 'POST' } })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const body = yield* Effect.promise(() => request.text()).pipe(
|
|
319
|
+
Effect.mapError(error => unknownToMessage(error)),
|
|
320
|
+
Effect.catch(error => badRequestBody(`Could not read request body: ${error}`))
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const responseBody = yield* handleJson(body).pipe(
|
|
324
|
+
Effect.catch(error => badRequestBody(unknownToMessage(error)))
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return jsonResponse(responseBody)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return { handleLine, handleJson, handleHttpRequest }
|
|
331
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Effect, Option, Stdio, Stream } from 'effect'
|
|
2
|
+
import type { PlatformError } from 'effect/PlatformError'
|
|
3
|
+
import type { McpToolServer } from './server.ts'
|
|
4
|
+
|
|
5
|
+
const writeStdout = (stdio: Stdio.Stdio, value: string) =>
|
|
6
|
+
Stream.make(`${value}\n`).pipe(Stream.run(stdio.stdout()))
|
|
7
|
+
|
|
8
|
+
const writeResponse = (stdio: Stdio.Stdio, response: Option.Option<string>) =>
|
|
9
|
+
Option.match(response, {
|
|
10
|
+
onNone: () => Effect.void,
|
|
11
|
+
onSome: value => writeStdout(stdio, value)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const ignoreStdioError = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
15
|
+
effect.pipe(Effect.catch(() => Effect.void))
|
|
16
|
+
|
|
17
|
+
const handleStdioLine = <R>(server: McpToolServer<R>, stdio: Stdio.Stdio, line: string) =>
|
|
18
|
+
server.handleLine(line).pipe(
|
|
19
|
+
Effect.flatMap(response => writeResponse(stdio, response)),
|
|
20
|
+
ignoreStdioError
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export const runStdioMcpServer = <R>(
|
|
24
|
+
server: McpToolServer<R>
|
|
25
|
+
): Effect.Effect<never, never, R | Stdio.Stdio> =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const stdio = yield* Stdio.Stdio
|
|
28
|
+
|
|
29
|
+
yield* stdio.stdin.pipe(
|
|
30
|
+
Stream.decodeText(),
|
|
31
|
+
Stream.splitLines,
|
|
32
|
+
Stream.runForEach(line => handleStdioLine(server, stdio, line)),
|
|
33
|
+
Effect.catch((_error: PlatformError) => Effect.void)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return yield* Effect.never
|
|
37
|
+
})
|