@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,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,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.
|