@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,364 @@
1
+ import { Array as Arr, Effect, Option } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import { AudioPart, ImagePart, TextPart, ToolDef, ToolResult } from '@yolk-sdk/agent/protocol'
4
+ import type { Content, ContentPart } from '@yolk-sdk/agent/protocol'
5
+ import { McpError } from './errors.ts'
6
+
7
+ export const latestMcpProtocolVersion = '2024-11-05'
8
+
9
+ export const JsonRpcErrorObject = Schema.Struct({
10
+ code: Schema.Number,
11
+ message: Schema.String,
12
+ data: Schema.optional(Schema.Unknown)
13
+ })
14
+ export type JsonRpcErrorObject = typeof JsonRpcErrorObject.Type
15
+
16
+ export const JsonRpcSuccessResponse = Schema.Struct({
17
+ jsonrpc: Schema.Literal('2.0'),
18
+ id: Schema.Union([Schema.String, Schema.Number, Schema.Null]),
19
+ result: Schema.Unknown
20
+ })
21
+ export type JsonRpcSuccessResponse = typeof JsonRpcSuccessResponse.Type
22
+
23
+ export const JsonRpcErrorResponse = Schema.Struct({
24
+ jsonrpc: Schema.Literal('2.0'),
25
+ id: Schema.Union([Schema.String, Schema.Number, Schema.Null]),
26
+ error: JsonRpcErrorObject
27
+ })
28
+ export type JsonRpcErrorResponse = typeof JsonRpcErrorResponse.Type
29
+
30
+ export const JsonRpcResponse = Schema.Union([JsonRpcSuccessResponse, JsonRpcErrorResponse])
31
+ export type JsonRpcResponse = typeof JsonRpcResponse.Type
32
+
33
+ export const JsonRpcRequest = Schema.Struct({
34
+ jsonrpc: Schema.Literal('2.0'),
35
+ id: Schema.Union([Schema.String, Schema.Number]),
36
+ method: Schema.String,
37
+ params: Schema.optional(Schema.Unknown)
38
+ })
39
+ export type JsonRpcRequest = typeof JsonRpcRequest.Type
40
+
41
+ export const JsonRpcNotification = Schema.Struct({
42
+ jsonrpc: Schema.Literal('2.0'),
43
+ method: Schema.String,
44
+ params: Schema.optional(Schema.Unknown)
45
+ })
46
+ export type JsonRpcNotification = typeof JsonRpcNotification.Type
47
+
48
+ export const JsonRpcMessage = Schema.Union([JsonRpcRequest, JsonRpcNotification])
49
+ export type JsonRpcMessage = typeof JsonRpcMessage.Type
50
+
51
+ export const McpTool = Schema.Struct({
52
+ name: Schema.String,
53
+ description: Schema.optional(Schema.String),
54
+ inputSchema: Schema.optional(Schema.Unknown)
55
+ })
56
+ export type McpTool = typeof McpTool.Type
57
+
58
+ export const ToolsListResult = Schema.Struct({
59
+ tools: Schema.Array(McpTool)
60
+ })
61
+ export type ToolsListResult = typeof ToolsListResult.Type
62
+
63
+ export const TextContentBlock = Schema.Struct({
64
+ type: Schema.Literal('text'),
65
+ text: Schema.String
66
+ })
67
+ export type TextContentBlock = typeof TextContentBlock.Type
68
+
69
+ export const GenericContentBlock = Schema.Record(Schema.String, Schema.Unknown)
70
+ export type GenericContentBlock = typeof GenericContentBlock.Type
71
+
72
+ const EmbeddedResourceContentBlock = Schema.Struct({
73
+ type: Schema.Literal('resource'),
74
+ resource: Schema.Struct({
75
+ uri: Schema.optional(Schema.String),
76
+ text: Schema.optional(Schema.String),
77
+ blob: Schema.optional(Schema.String),
78
+ mimeType: Schema.optional(Schema.String)
79
+ })
80
+ })
81
+
82
+ const ResourceLinkContentBlock = Schema.Struct({
83
+ type: Schema.Literal('resource_link'),
84
+ uri: Schema.String,
85
+ name: Schema.optional(Schema.String),
86
+ mimeType: Schema.optional(Schema.String)
87
+ })
88
+
89
+ export const ToolCallResult = Schema.Struct({
90
+ content: Schema.optional(Schema.Array(GenericContentBlock)),
91
+ isError: Schema.optional(Schema.Boolean),
92
+ structuredContent: Schema.optional(Schema.Unknown)
93
+ })
94
+ export type ToolCallResult = typeof ToolCallResult.Type
95
+
96
+ export const makeJsonRpcRequest = (input: {
97
+ readonly id: string | number
98
+ readonly method: string
99
+ readonly params?: unknown
100
+ }): JsonRpcRequest => ({
101
+ jsonrpc: '2.0',
102
+ id: input.id,
103
+ method: input.method,
104
+ ...(input.params === undefined ? {} : { params: input.params })
105
+ })
106
+
107
+ export const makeInitializedNotification = (): JsonRpcNotification => ({
108
+ jsonrpc: '2.0',
109
+ method: 'notifications/initialized'
110
+ })
111
+
112
+ export const makeInitializeParams = (input: {
113
+ readonly name: string
114
+ readonly version: string
115
+ }) => ({
116
+ protocolVersion: latestMcpProtocolVersion,
117
+ capabilities: {},
118
+ clientInfo: {
119
+ name: input.name,
120
+ version: input.version
121
+ }
122
+ })
123
+
124
+ export const jsonRpcErrorToMcpError = (server: string, error: JsonRpcErrorObject) =>
125
+ new McpError({
126
+ server,
127
+ message: `MCP JSON-RPC error ${error.code}: ${error.message}`,
128
+ cause: 'protocol'
129
+ })
130
+
131
+ export const mcpToolToToolDef = (input: { readonly serverName: string; readonly tool: McpTool }) =>
132
+ ToolDef.make({
133
+ name: `${sanitizeMcpName(input.serverName)}_${sanitizeMcpName(input.tool.name)}`,
134
+ description: input.tool.description ?? `MCP tool ${input.serverName}/${input.tool.name}`,
135
+ parameters: input.tool.inputSchema ?? { type: 'object', additionalProperties: true }
136
+ })
137
+
138
+ export const sanitizeMcpName = (name: string) => {
139
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
140
+ return sanitized.length === 0 ? 'mcp' : sanitized
141
+ }
142
+
143
+ const stringProperty = (block: GenericContentBlock, key: string) => {
144
+ const value = block[key]
145
+
146
+ return typeof value === 'string' ? Option.some(value) : Option.none<string>()
147
+ }
148
+
149
+ const audioFormatFromMimeType = (
150
+ mimeType: string
151
+ ): Option.Option<'pcm16' | 'wav' | 'mp3' | 'opus'> => {
152
+ switch (mimeType) {
153
+ case 'audio/pcm':
154
+ case 'audio/pcm16':
155
+ return Option.some('pcm16')
156
+ case 'audio/wav':
157
+ case 'audio/wave':
158
+ case 'audio/x-wav':
159
+ return Option.some('wav')
160
+ case 'audio/mpeg':
161
+ case 'audio/mp3':
162
+ return Option.some('mp3')
163
+ case 'audio/opus':
164
+ case 'audio/ogg; codecs=opus':
165
+ return Option.some('opus')
166
+ default:
167
+ return Option.none<'pcm16' | 'wav' | 'mp3' | 'opus'>()
168
+ }
169
+ }
170
+
171
+ const contentBlockText = (block: GenericContentBlock): Option.Option<string> => {
172
+ const type = block['type']
173
+ const text = stringProperty(block, 'text')
174
+
175
+ if (type === 'text') {
176
+ return text
177
+ }
178
+
179
+ return Option.none()
180
+ }
181
+
182
+ const fallbackTextForContentBlock = (block: GenericContentBlock) => {
183
+ const type = stringProperty(block, 'type').pipe(Option.getOrElse(() => 'unknown'))
184
+ const name = stringProperty(block, 'name')
185
+ const uri = stringProperty(block, 'uri')
186
+ const url = stringProperty(block, 'url')
187
+ const label = Option.getOrElse(Option.firstSomeOf([name, uri, url]), () => type)
188
+
189
+ switch (type) {
190
+ case 'resource':
191
+ return `MCP resource: ${label}`
192
+ case 'resource_link':
193
+ return `MCP resource link: ${label}`
194
+ case 'image':
195
+ return `MCP image: ${label}`
196
+ case 'audio':
197
+ return `MCP audio: ${label}`
198
+ default:
199
+ return `Unsupported MCP ${type} content.`
200
+ }
201
+ }
202
+
203
+ const embeddedResourceText = (block: GenericContentBlock): Option.Option<string> =>
204
+ Schema.decodeUnknownOption(EmbeddedResourceContentBlock)(block).pipe(
205
+ Option.flatMap(({ resource }) => {
206
+ if (resource.text !== undefined) {
207
+ return Option.some(resource.text)
208
+ }
209
+
210
+ return Option.all({
211
+ uri: Option.fromNullishOr(resource.uri),
212
+ blob: Option.fromNullishOr(resource.blob)
213
+ }).pipe(Option.map(({ uri, blob }) => `MCP resource: ${uri}\n${blob}`))
214
+ })
215
+ )
216
+
217
+ const resourceLinkText = (block: GenericContentBlock): Option.Option<string> =>
218
+ Schema.decodeUnknownOption(ResourceLinkContentBlock)(block).pipe(
219
+ Option.map(({ name, uri }) => {
220
+ const label = name ?? uri
221
+ return `MCP resource link: ${label} (${uri})`
222
+ })
223
+ )
224
+
225
+ const imagePartFromBlock = (block: GenericContentBlock): Option.Option<ContentPart> =>
226
+ Option.all({
227
+ data: stringProperty(block, 'data'),
228
+ mimeType: stringProperty(block, 'mimeType')
229
+ }).pipe(Option.map(({ data, mimeType }) => ImagePart.make({ data, mimeType })))
230
+
231
+ const audioPartFromBlock = (block: GenericContentBlock): Option.Option<ContentPart> =>
232
+ Option.all({
233
+ data: stringProperty(block, 'data'),
234
+ mimeType: stringProperty(block, 'mimeType')
235
+ }).pipe(
236
+ Option.flatMap(({ data, mimeType }) =>
237
+ audioFormatFromMimeType(mimeType).pipe(Option.map(format => AudioPart.make({ data, format })))
238
+ )
239
+ )
240
+
241
+ const contentPartFromBlock = (block: GenericContentBlock): ContentPart => {
242
+ const type = block['type']
243
+
244
+ if (type === 'text') {
245
+ return TextPart.make({ text: Option.getOrElse(contentBlockText(block), () => '') })
246
+ }
247
+
248
+ if (type === 'image') {
249
+ return Option.getOrElse(imagePartFromBlock(block), () =>
250
+ TextPart.make({ text: fallbackTextForContentBlock(block) })
251
+ )
252
+ }
253
+
254
+ if (type === 'audio') {
255
+ return Option.getOrElse(audioPartFromBlock(block), () =>
256
+ TextPart.make({ text: fallbackTextForContentBlock(block) })
257
+ )
258
+ }
259
+
260
+ if (type === 'resource') {
261
+ return TextPart.make({
262
+ text: embeddedResourceText(block).pipe(
263
+ Option.getOrElse(() => fallbackTextForContentBlock(block))
264
+ )
265
+ })
266
+ }
267
+
268
+ if (type === 'resource_link') {
269
+ return TextPart.make({
270
+ text: resourceLinkText(block).pipe(Option.getOrElse(() => fallbackTextForContentBlock(block)))
271
+ })
272
+ }
273
+
274
+ return TextPart.make({ text: fallbackTextForContentBlock(block) })
275
+ }
276
+
277
+ const contentFromBlocks = (blocks: ReadonlyArray<GenericContentBlock>): Content => {
278
+ const textBlocks = Arr.getSomes(Arr.map(blocks, contentBlockText))
279
+
280
+ if (textBlocks.length === blocks.length) {
281
+ return textBlocks.join('\n')
282
+ }
283
+
284
+ return Arr.map(blocks, contentPartFromBlock)
285
+ }
286
+
287
+ export const toolCallResultToToolResult = (input: {
288
+ readonly toolCallId: string
289
+ readonly result: ToolCallResult
290
+ }) => {
291
+ const content = input.result.content ?? []
292
+ const resultContent =
293
+ content.length > 0
294
+ ? contentFromBlocks(content)
295
+ : input.result.structuredContent === undefined
296
+ ? 'Unsupported MCP tool content.'
297
+ : 'Structured MCP tool result.'
298
+
299
+ return ToolResult.make({
300
+ toolCallId: input.toolCallId,
301
+ content: resultContent,
302
+ isError: input.result.isError,
303
+ structuredContent: input.result.structuredContent
304
+ })
305
+ }
306
+
307
+ export const decodeJsonRpcResponse = Schema.decodeUnknownEffect(JsonRpcResponse)
308
+ export const decodeToolsListResult = Schema.decodeUnknownEffect(ToolsListResult)
309
+ export const decodeToolCallResult = Schema.decodeUnknownEffect(ToolCallResult)
310
+
311
+ export const encodeJsonRpcMessage = (
312
+ server: string,
313
+ message: JsonRpcRequest | JsonRpcNotification
314
+ ) =>
315
+ Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)(message).pipe(
316
+ Effect.mapError(
317
+ error =>
318
+ new McpError({
319
+ server,
320
+ message: `Could not encode MCP JSON-RPC message: ${String(error)}`,
321
+ cause: 'encoding'
322
+ })
323
+ )
324
+ )
325
+
326
+ const decodeJsonString = (server: string, text: string) =>
327
+ Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(text).pipe(
328
+ Effect.mapError(
329
+ error =>
330
+ new McpError({
331
+ server,
332
+ message: `Malformed MCP JSON: ${String(error)}`,
333
+ cause: 'parse'
334
+ })
335
+ )
336
+ )
337
+
338
+ export const decodeJsonRpcResponseFromJson = (server: string, text: string) =>
339
+ decodeJsonString(server, text).pipe(
340
+ Effect.flatMap(decodeJsonRpcResponse),
341
+ Effect.mapError(error =>
342
+ error instanceof McpError
343
+ ? error
344
+ : new McpError({
345
+ server,
346
+ message: `Invalid MCP JSON-RPC response: ${String(error)}`,
347
+ cause: 'validation'
348
+ })
349
+ )
350
+ )
351
+
352
+ export const decodeJsonRpcMessageFromJson = (server: string, text: string) =>
353
+ decodeJsonString(server, text).pipe(
354
+ Effect.flatMap(Schema.decodeUnknownEffect(JsonRpcMessage)),
355
+ Effect.mapError(error =>
356
+ error instanceof McpError
357
+ ? error
358
+ : new McpError({
359
+ server,
360
+ message: `Invalid MCP JSON-RPC message: ${String(error)}`,
361
+ cause: 'validation'
362
+ })
363
+ )
364
+ )
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Root intentionally empty. Import client/server APIs from explicit subpaths.
2
+ export {}
@@ -0,0 +1,22 @@
1
+ # @yolk-sdk/mcp/server
2
+
3
+ Reusable tool-only MCP server primitives.
4
+
5
+ ## What it provides
6
+
7
+ - MCP `initialize`, `tools/list`, and `tools/call` handling.
8
+ - HTTP POST JSON-RPC entrypoint.
9
+ - Newline-delimited stdio runner over Effect `Stdio`.
10
+ - Tool-only server creation from generic Yolk tool handlers.
11
+
12
+ ## Use it when
13
+
14
+ - You want to expose Yolk-compatible tools through MCP.
15
+ - You need a minimal MCP tool server for app routes, CLIs, or tests.
16
+
17
+ ## Boundaries
18
+
19
+ - No MCP resources or prompts.
20
+ - No OAuth/app auth.
21
+ - No framework-specific server dependency.
22
+ - Node stdio CLIs provide `NodeStdio.layer` at the boundary.
@@ -0,0 +1,6 @@
1
+ import * as Schema from 'effect/Schema'
2
+
3
+ export class McpServerError extends Schema.TaggedErrorClass<McpServerError>()('McpServerError', {
4
+ message: Schema.String,
5
+ cause: Schema.Literals(['parse', 'validation', 'protocol', 'tool_error', 'encoding'])
6
+ }) {}
@@ -0,0 +1,4 @@
1
+ export { McpServerError } from './errors.ts'
2
+ export { makeMcpToolServer } from './server.ts'
3
+ export type { McpServerTool, McpToolServer } from './server.ts'
4
+ export { runStdioMcpServer } from './stdio.ts'