@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/client/client.d.mts +40 -0
  4. package/dist/client/client.d.mts.map +1 -0
  5. package/dist/client/client.mjs +225 -0
  6. package/dist/client/client.mjs.map +1 -0
  7. package/dist/client/config.d.mts +29 -0
  8. package/dist/client/config.d.mts.map +1 -0
  9. package/dist/client/config.mjs +13 -0
  10. package/dist/client/config.mjs.map +1 -0
  11. package/dist/client/errors.d.mts +14 -0
  12. package/dist/client/errors.d.mts.map +1 -0
  13. package/dist/client/errors.mjs +22 -0
  14. package/dist/client/errors.mjs.map +1 -0
  15. package/dist/client/index.d.mts +5 -0
  16. package/dist/client/index.mjs +5 -0
  17. package/dist/client/node.d.mts +16 -0
  18. package/dist/client/node.d.mts.map +1 -0
  19. package/dist/client/node.mjs +13 -0
  20. package/dist/client/node.mjs.map +1 -0
  21. package/dist/client/protocol.d.mts +175 -0
  22. package/dist/client/protocol.d.mts.map +1 -0
  23. package/dist/client/protocol.mjs +225 -0
  24. package/dist/client/protocol.mjs.map +1 -0
  25. package/dist/index.d.mts +1 -0
  26. package/dist/index.mjs +1 -0
  27. package/dist/server/errors.d.mts +11 -0
  28. package/dist/server/errors.d.mts.map +1 -0
  29. package/dist/server/errors.mjs +16 -0
  30. package/dist/server/errors.mjs.map +1 -0
  31. package/dist/server/index.d.mts +4 -0
  32. package/dist/server/index.mjs +4 -0
  33. package/dist/server/server.d.mts +22 -0
  34. package/dist/server/server.d.mts.map +1 -0
  35. package/dist/server/server.mjs +180 -0
  36. package/dist/server/server.mjs.map +1 -0
  37. package/dist/server/stdio.d.mts +8 -0
  38. package/dist/server/stdio.d.mts.map +1 -0
  39. package/dist/server/stdio.mjs +18 -0
  40. package/dist/server/stdio.mjs.map +1 -0
  41. package/package.json +77 -0
  42. package/src/client/README.md +24 -0
  43. package/src/client/client.ts +494 -0
  44. package/src/client/config.ts +37 -0
  45. package/src/client/errors.ts +20 -0
  46. package/src/client/index.ts +48 -0
  47. package/src/client/node.ts +32 -0
  48. package/src/client/protocol.ts +364 -0
  49. package/src/index.ts +2 -0
  50. package/src/server/README.md +22 -0
  51. package/src/server/errors.ts +6 -0
  52. package/src/server/index.ts +4 -0
  53. package/src/server/server.ts +331 -0
  54. 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
+ })