ice-jade 0.7.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f57e6758c88b25a57a7891bed83b7416ae9f511e780bcde4be30bb6f211ee3b3
4
- data.tar.gz: 902380ad3fb6f0bfd2bd21f171bc3aa99a9538590455ba4a41f2b582a8a0d326
3
+ metadata.gz: 8b39e51e5e392d1d9ffd1ee3c991a0e22ea515a03b3ce3f2450642c8b460e595
4
+ data.tar.gz: 960d1cb2bd5f82023ed99de8ceff20dff679356a409086aecf89b0884b3895a7
5
5
  SHA512:
6
- metadata.gz: 14d027172321795ad155cc0cd3bbb8f196d18db00b20c9d51cb5c990c84184633bafffd637e7d50c10c1018e70319efc9b136de35b8e1bd364d112a84d79a10a
7
- data.tar.gz: e7ddcc89ef4fde4cd0ee17f9ee81d03f4c7fec1f6c8a0c5329741bbf298d77422131c40b86dadb941b93ff6256409b80366c8fd3cd9fc6bac7601601841fc7ca
6
+ metadata.gz: 8f3a1b54514dde13911ab21a994c6b1b0ff4a1e66facc77328fd69459406090c86eb773bbb7ab24726b31c784a1ec2140703cec161aaecd30053132c4bbf03c9
7
+ data.tar.gz: 28e26d266c4df37adfc552f4edcab9c883c6922c68196cf40532131571fa88004d225aa2b38e6b1f234b75c6c3f12adf2851d22ed3de21245e9b3035cb74a10e
data/bin/mcp_server.rb ADDED
@@ -0,0 +1,551 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ice-jade MCP Server (zero-dependency, stdio JSON-RPC 2.0)
4
+ #
5
+ # 不依赖 mcp gem,仅使用 Ruby 标准库实现 MCP 协议。
6
+ # 覆盖模块:Quantum / Poster / Getter / HttpPoster / HttpGetter / Cradle
7
+ #
8
+ # 用法:
9
+ # ruby bin/mcp_server.rb
10
+ #
11
+ # 在 Claude Desktop / Cursor 等客户端的 MCP 配置中:
12
+ # {
13
+ # "mcpServers": {
14
+ # "ice-jade": {
15
+ # "command": "ruby",
16
+ # "args": ["E:\\path\\to\\ice-jade\\bin\\mcp_server.rb"]
17
+ # }
18
+ # }
19
+ # }
20
+
21
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
22
+
23
+ require 'ice_jade'
24
+ require_relative '../lib/http_poster'
25
+ require_relative '../lib/http_getter'
26
+ require 'json'
27
+ require 'uri'
28
+
29
+ # ============================================================
30
+ # 工具方法
31
+ # ============================================================
32
+ module MCPHelper
33
+ module_function
34
+
35
+ def format_response(resp)
36
+ {
37
+ success: resp.success?,
38
+ code: resp.code,
39
+ ok: resp.ok,
40
+ message: resp.message,
41
+ data: resp.data
42
+ }.to_json
43
+ end
44
+
45
+ def format_error(e)
46
+ { error: true, message: e.message, class: e.class.name }.to_json
47
+ end
48
+
49
+ def symbolize_keys(hash)
50
+ return hash unless hash.is_a?(Hash)
51
+ hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = symbolize_keys(v) }
52
+ end
53
+
54
+ # 构造 MCP text content block
55
+ def text_content(json_str)
56
+ [{ type: 'text', text: json_str }]
57
+ end
58
+
59
+ # 构造 MCP tool call 成功响应
60
+ def ok_result(json_str)
61
+ { content: text_content(json_str) }
62
+ end
63
+
64
+ # 构造 MCP tool call 失败响应(isError: true)
65
+ def error_result(json_str)
66
+ { content: text_content(json_str), isError: true }
67
+ end
68
+ end
69
+
70
+ # ============================================================
71
+ # 工具注册表
72
+ # ============================================================
73
+
74
+ # 运行中的 Cradle 服务器实例(避免被 GC 回收)
75
+ RUNNING_SERVERS = {}
76
+
77
+ # 每个工具是一个 Hash:{ name:, description:, input_schema:, handler: }
78
+ TOOLS = []
79
+
80
+ def register_tool(name, description, input_schema, &handler)
81
+ TOOLS << { name: name, description: description, inputSchema: input_schema, handler: handler }
82
+ end
83
+
84
+ # ============================================================
85
+ # 1. Quantum — IM 消息发送
86
+ # ============================================================
87
+
88
+ register_tool('quantum_send_text',
89
+ '通过 Quantum IM 机器人向群聊发送文本消息,支持@所有人或指定手机号',
90
+ {
91
+ type: 'object',
92
+ properties: {
93
+ webhook_key: { type: 'string', description: 'Quantum 机器人的 webhook key' },
94
+ content: { type: 'string', description: '要发送的文本内容' },
95
+ mention_all: { type: 'boolean', description: '是否@所有人,默认 false' },
96
+ base_url: { type: 'string', description: '可选,覆盖默认的 webhook 基础 URL' }
97
+ },
98
+ required: %w[webhook_key content]
99
+ }
100
+ ) do |args|
101
+ client = args['base_url'] ?
102
+ IceJade::Quantum::Client.new(args['webhook_key'], base_url: args['base_url']) :
103
+ IceJade::Quantum::Client.new(args['webhook_key'])
104
+ resp = client.send_text(args['content'], mention_all: args.fetch('mention_all', false))
105
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
106
+ rescue StandardError => e
107
+ MCPHelper.error_result(MCPHelper.format_error(e))
108
+ end
109
+
110
+ register_tool('quantum_send_news',
111
+ '通过 Quantum IM 机器人发送图文链接卡片消息',
112
+ {
113
+ type: 'object',
114
+ properties: {
115
+ webhook_key: { type: 'string', description: 'Quantum 机器人的 webhook key' },
116
+ title: { type: 'string', description: '卡片标题' },
117
+ url: { type: 'string', description: '点击卡片跳转的链接' },
118
+ description: { type: 'string', description: '可选,卡片描述文本' },
119
+ pic_url: { type: 'string', description: '可选,卡片配图 URL' },
120
+ base_url: { type: 'string', description: '可选,覆盖默认的 webhook 基础 URL' }
121
+ },
122
+ required: %w[webhook_key title url]
123
+ }
124
+ ) do |args|
125
+ client = args['base_url'] ?
126
+ IceJade::Quantum::Client.new(args['webhook_key'], base_url: args['base_url']) :
127
+ IceJade::Quantum::Client.new(args['webhook_key'])
128
+ resp = client.send_news(args['title'], args['url'], description: args['description'], pic_url: args['pic_url'])
129
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
130
+ rescue StandardError => e
131
+ MCPHelper.error_result(MCPHelper.format_error(e))
132
+ end
133
+
134
+ register_tool('quantum_upload_and_send_image',
135
+ '上传本地图片到 Quantum IM 并立即发送到群聊(两步合一)',
136
+ {
137
+ type: 'object',
138
+ properties: {
139
+ webhook_key: { type: 'string', description: 'Quantum 机器人的 webhook key' },
140
+ path: { type: 'string', description: '本地图片文件的完整路径' },
141
+ height: { type: 'integer', description: '图片高度(像素)' },
142
+ width: { type: 'integer', description: '图片宽度(像素)' },
143
+ base_url: { type: 'string', description: '可选,覆盖默认的 webhook 基础 URL' }
144
+ },
145
+ required: %w[webhook_key path height width]
146
+ }
147
+ ) do |args|
148
+ client = args['base_url'] ?
149
+ IceJade::Quantum::Client.new(args['webhook_key'], base_url: args['base_url']) :
150
+ IceJade::Quantum::Client.new(args['webhook_key'])
151
+ resp = client.upload_and_send_image(args['path'], height: args['height'], width: args['width'])
152
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
153
+ rescue StandardError => e
154
+ MCPHelper.error_result(MCPHelper.format_error(e))
155
+ end
156
+
157
+ register_tool('quantum_upload_and_send_file',
158
+ '上传本地文件到 Quantum IM 并立即发送到群聊(两步合一)',
159
+ {
160
+ type: 'object',
161
+ properties: {
162
+ webhook_key: { type: 'string', description: 'Quantum 机器人的 webhook key' },
163
+ path: { type: 'string', description: '本地文件的完整路径' },
164
+ base_url: { type: 'string', description: '可选,覆盖默认的 webhook 基础 URL' }
165
+ },
166
+ required: %w[webhook_key path]
167
+ }
168
+ ) do |args|
169
+ client = args['base_url'] ?
170
+ IceJade::Quantum::Client.new(args['webhook_key'], base_url: args['base_url']) :
171
+ IceJade::Quantum::Client.new(args['webhook_key'])
172
+ resp = client.upload_and_send_file(args['path'])
173
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
174
+ rescue StandardError => e
175
+ MCPHelper.error_result(MCPHelper.format_error(e))
176
+ end
177
+
178
+ # ============================================================
179
+ # 2. Poster — 通用 HTTP POST 客户端
180
+ # ============================================================
181
+
182
+ register_tool('poster_post_json',
183
+ '发送 HTTP POST JSON 请求。支持 base_url + 相对路径或完整 URL,内置超时重试',
184
+ {
185
+ type: 'object',
186
+ properties: {
187
+ url: { type: 'string', description: '完整 URL 或相对路径(需配合 base_url)' },
188
+ params: { type: 'object', description: '要 POST 的 JSON 参数对象' },
189
+ base_url: { type: 'string', description: '可选,基础 URL,设置后 url 可用相对路径' },
190
+ headers: { type: 'object', description: '可选,额外请求头' },
191
+ timeout: { type: 'integer', description: '可选,读取超时秒数,默认 60' }
192
+ },
193
+ required: %w[url]
194
+ }
195
+ ) do |args|
196
+ poster = IceJade::Poster::Client.new(
197
+ base_url: args['base_url'], headers: args.fetch('headers', {}), timeout: args.fetch('timeout', 60)
198
+ )
199
+ resp = poster.post_json(args['url'], args.fetch('params', {}))
200
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
201
+ rescue StandardError => e
202
+ MCPHelper.error_result(MCPHelper.format_error(e))
203
+ end
204
+
205
+ register_tool('poster_post_form',
206
+ '发送 HTTP POST Form 表单请求(application/x-www-form-urlencoded)',
207
+ {
208
+ type: 'object',
209
+ properties: {
210
+ url: { type: 'string', description: '完整 URL 或相对路径' },
211
+ params: { type: 'object', description: '表单参数对象' },
212
+ base_url: { type: 'string', description: '可选,基础 URL' },
213
+ headers: { type: 'object', description: '可选,额外请求头' },
214
+ timeout: { type: 'integer', description: '可选,读取超时秒数,默认 60' }
215
+ },
216
+ required: %w[url]
217
+ }
218
+ ) do |args|
219
+ poster = IceJade::Poster::Client.new(
220
+ base_url: args['base_url'], headers: args.fetch('headers', {}), timeout: args.fetch('timeout', 60)
221
+ )
222
+ resp = poster.post_form(args['url'], args.fetch('params', {}))
223
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
224
+ rescue StandardError => e
225
+ MCPHelper.error_result(MCPHelper.format_error(e))
226
+ end
227
+
228
+ register_tool('poster_post_multipart',
229
+ '发送 HTTP POST Multipart 请求(文件上传)。params 中值为文件路径的字段会自动读取文件',
230
+ {
231
+ type: 'object',
232
+ properties: {
233
+ url: { type: 'string', description: '完整 URL 或相对路径' },
234
+ params: { type: 'object', description: '参数对象,值为文件路径的字段会自动上传该文件' },
235
+ base_url: { type: 'string', description: '可选,基础 URL' },
236
+ headers: { type: 'object', description: '可选,额外请求头' }
237
+ },
238
+ required: %w[url params]
239
+ }
240
+ ) do |args|
241
+ poster = IceJade::Poster::Client.new(base_url: args['base_url'], headers: args.fetch('headers', {}))
242
+ resp = poster.post_multipart(args['url'], MCPHelper.symbolize_keys(args['params']))
243
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
244
+ rescue StandardError => e
245
+ MCPHelper.error_result(MCPHelper.format_error(e))
246
+ end
247
+
248
+ # ============================================================
249
+ # 3. Getter — 通用 HTTP GET 客户端
250
+ # ============================================================
251
+
252
+ register_tool('getter_get',
253
+ '发送 HTTP GET 请求。支持查询参数、自定义请求头,内置超时重试。返回统一 Response 包装',
254
+ {
255
+ type: 'object',
256
+ properties: {
257
+ url: { type: 'string', description: '完整 URL 或相对路径(需配合 base_url)' },
258
+ params: { type: 'object', description: '可选,查询参数对象,自动编码到 query string' },
259
+ base_url: { type: 'string', description: '可选,基础 URL' },
260
+ headers: { type: 'object', description: '可选,额外请求头' },
261
+ timeout: { type: 'integer', description: '可选,读取超时秒数,默认 60' }
262
+ },
263
+ required: %w[url]
264
+ }
265
+ ) do |args|
266
+ getter = IceJade::Getter::Client.new(
267
+ base_url: args['base_url'], headers: args.fetch('headers', {}), timeout: args.fetch('timeout', 60)
268
+ )
269
+ resp = getter.get(args['url'], params: args['params'])
270
+ MCPHelper.ok_result(MCPHelper.format_response(resp))
271
+ rescue StandardError => e
272
+ MCPHelper.error_result(MCPHelper.format_error(e))
273
+ end
274
+
275
+ # ============================================================
276
+ # 4. HttpPoster — 独立模块级 POST 工具
277
+ # ============================================================
278
+
279
+ register_tool('http_poster_json',
280
+ '使用 HttpPoster 模块发送 POST JSON 请求(独立模块,零 gem 依赖)。返回解析后的 JSON Hash',
281
+ {
282
+ type: 'object',
283
+ properties: {
284
+ url: { type: 'string', description: '完整的请求 URL' },
285
+ params: { type: 'object', description: '要 POST 的 JSON 参数对象' },
286
+ headers: { type: 'object', description: '可选,自定义请求头(如 Authorization)' },
287
+ timeout: { type: 'integer', description: '可选,超时秒数,默认 60' }
288
+ },
289
+ required: %w[url]
290
+ }
291
+ ) do |args|
292
+ result = HttpPoster.post_json(
293
+ args['url'], args.fetch('params', {}), args.fetch('headers', {}), { timeout: args.fetch('timeout', 60) }
294
+ )
295
+ MCPHelper.ok_result(result.is_a?(String) ? result : result.to_json)
296
+ rescue StandardError => e
297
+ MCPHelper.error_result(MCPHelper.format_error(e))
298
+ end
299
+
300
+ register_tool('http_poster_multipart',
301
+ '使用 HttpPoster 模块发送 POST Multipart 文件上传请求。params 中值为文件路径的字段会自动读取文件',
302
+ {
303
+ type: 'object',
304
+ properties: {
305
+ url: { type: 'string', description: '完整的请求 URL' },
306
+ params: { type: 'object', description: '参数对象,值为文件路径的字段自动上传文件' },
307
+ headers: { type: 'object', description: '可选,自定义请求头' }
308
+ },
309
+ required: %w[url params]
310
+ }
311
+ ) do |args|
312
+ result = HttpPoster.post_multipart(args['url'], MCPHelper.symbolize_keys(args['params']), args.fetch('headers', {}))
313
+ MCPHelper.ok_result(result.is_a?(String) ? result : result.to_json)
314
+ rescue StandardError => e
315
+ MCPHelper.error_result(MCPHelper.format_error(e))
316
+ end
317
+
318
+ # ============================================================
319
+ # 5. HttpGetter — 独立模块级 GET 工具
320
+ # ============================================================
321
+
322
+ register_tool('http_getter_json',
323
+ '使用 HttpGetter 模块发送 GET JSON 请求(独立模块,零 gem 依赖)。返回解析后的 JSON Hash',
324
+ {
325
+ type: 'object',
326
+ properties: {
327
+ url: { type: 'string', description: '完整的请求 URL' },
328
+ params: { type: 'object', description: '可选,查询参数对象' },
329
+ headers: { type: 'object', description: '可选,自定义请求头' },
330
+ timeout: { type: 'integer', description: '可选,超时秒数,默认 60' }
331
+ },
332
+ required: %w[url]
333
+ }
334
+ ) do |args|
335
+ result = HttpGetter.get_json(
336
+ args['url'], args.fetch('params', {}), args.fetch('headers', {}), { timeout: args.fetch('timeout', 60) }
337
+ )
338
+ MCPHelper.ok_result(result.is_a?(String) ? result : result.to_json)
339
+ rescue StandardError => e
340
+ MCPHelper.error_result(MCPHelper.format_error(e))
341
+ end
342
+
343
+ register_tool('http_getter_text',
344
+ '使用 HttpGetter 模块发送 GET 请求并返回纯文本(不做 JSON 解析)。适合获取 HTML、robots.txt 等',
345
+ {
346
+ type: 'object',
347
+ properties: {
348
+ url: { type: 'string', description: '完整的请求 URL' },
349
+ params: { type: 'object', description: '可选,查询参数对象' },
350
+ headers: { type: 'object', description: '可选,自定义请求头' }
351
+ },
352
+ required: %w[url]
353
+ }
354
+ ) do |args|
355
+ result = HttpGetter.get_text(args['url'], args.fetch('params', {}), args.fetch('headers', {}))
356
+ MCPHelper.ok_result(result.to_s)
357
+ rescue StandardError => e
358
+ MCPHelper.error_result(MCPHelper.format_error(e))
359
+ end
360
+
361
+ # ============================================================
362
+ # 6. Cradle — HTTP 测试服务器
363
+ # ============================================================
364
+
365
+ register_tool('cradle_start_server',
366
+ '启动 Cradle HTTP 测试服务器。返回服务器信息(端口、地址),服务器在独立线程中运行不会阻塞',
367
+ {
368
+ type: 'object',
369
+ properties: {
370
+ port: { type: 'integer', description: '监听端口,默认 8765' },
371
+ host: { type: 'string', description: '监听地址,默认 0.0.0.0' }
372
+ }
373
+ }
374
+ ) do |args|
375
+ port = args.fetch('port', 8765)
376
+ host = args.fetch('host', '0.0.0.0')
377
+ key = "#{host}:#{port}"
378
+ if RUNNING_SERVERS[key]
379
+ MCPHelper.ok_result({ already_running: true, host: host, port: port }.to_json)
380
+ else
381
+ server = IceJade::Cradle::Server.new(port: port, host: host, silent: true)
382
+ thread = Thread.new { server.start }
383
+ RUNNING_SERVERS[key] = { server: server, thread: thread }
384
+ MCPHelper.ok_result({ started: true, host: host, port: port, message: 'Cradle 测试服务器已启动' }.to_json)
385
+ end
386
+ rescue StandardError => e
387
+ MCPHelper.error_result(MCPHelper.format_error(e))
388
+ end
389
+
390
+ register_tool('cradle_stop_server',
391
+ '停止运行中的 Cradle HTTP 测试服务器',
392
+ {
393
+ type: 'object',
394
+ properties: {
395
+ port: { type: 'integer', description: '要停止的服务器端口,默认 8765' },
396
+ host: { type: 'string', description: '要停止的服务器地址,默认 0.0.0.0' }
397
+ }
398
+ }
399
+ ) do |args|
400
+ port = args.fetch('port', 8765)
401
+ host = args.fetch('host', '0.0.0.0')
402
+ key = "#{host}:#{port}"
403
+ entry = RUNNING_SERVERS.delete(key)
404
+ if entry
405
+ entry[:server].stop
406
+ entry[:thread].kill
407
+ MCPHelper.ok_result({ stopped: true, host: host, port: port }.to_json)
408
+ else
409
+ MCPHelper.ok_result({ stopped: false, message: "未找到运行中的服务器 #{key}" }.to_json)
410
+ end
411
+ rescue StandardError => e
412
+ MCPHelper.error_result(MCPHelper.format_error(e))
413
+ end
414
+
415
+ register_tool('cradle_add_route',
416
+ '向运行中的 Cradle 测试服务器添加自定义路由,用于模拟 API 响应',
417
+ {
418
+ type: 'object',
419
+ properties: {
420
+ port: { type: 'integer', description: '目标服务器端口,默认 8765' },
421
+ host: { type: 'string', description: '目标服务器地址,默认 0.0.0.0' },
422
+ method: { type: 'string', description: 'HTTP 方法:GET / POST / PUT / DELETE 等' },
423
+ path: { type: 'string', description: '路由路径,如 /echo 或 /api/users/:id' },
424
+ status: { type: 'integer', description: '响应状态码,默认 200' },
425
+ body: { type: 'string', description: '响应体内容' },
426
+ content_type: { type: 'string', description: '响应 Content-Type,默认 application/json' }
427
+ },
428
+ required: %w[method path]
429
+ }
430
+ ) do |args|
431
+ port = args.fetch('port', 8765)
432
+ host = args.fetch('host', '0.0.0.0')
433
+ key = "#{host}:#{port}"
434
+ entry = RUNNING_SERVERS[key]
435
+ raise "服务器 #{key} 未运行" unless entry
436
+
437
+ server = entry[:server]
438
+ m = args['method'].to_sym
439
+ route_path = args['path']
440
+ route_status = args.fetch('status', 200)
441
+ route_body = args.fetch('body', '{}')
442
+ route_ct = args.fetch('content_type', 'application/json')
443
+ server.route(m, route_path) do |_req|
444
+ [route_status, { 'Content-Type' => route_ct }, route_body]
445
+ end
446
+ MCPHelper.ok_result({ added: true, method: args['method'].upcase, path: route_path, status: route_status }.to_json)
447
+ rescue StandardError => e
448
+ MCPHelper.error_result(MCPHelper.format_error(e))
449
+ end
450
+
451
+ # ============================================================
452
+ # MCP 协议层 — 纯标准库 JSON-RPC 2.0 over STDIO
453
+ # ============================================================
454
+
455
+ PROTOCOL_VERSION = '2025-03-26'
456
+ SERVER_NAME = 'ice-jade-mcp-server'
457
+ SERVER_VERSION = IceJade::VERSION
458
+
459
+ def handle_initialize(req)
460
+ {
461
+ protocolVersion: PROTOCOL_VERSION,
462
+ capabilities: {
463
+ tools: { listChanged: false }
464
+ },
465
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
466
+ }
467
+ end
468
+
469
+ def handle_tools_list(req)
470
+ { tools: TOOLS.map { |t| { name: t[:name], description: t[:description], inputSchema: t[:inputSchema] } } }
471
+ end
472
+
473
+ def handle_tools_call(req)
474
+ params = req['params'] || {}
475
+ tool_name = params['name']
476
+ arguments = params['arguments'] || {}
477
+ tool = TOOLS.find { |t| t[:name] == tool_name }
478
+
479
+ if tool.nil?
480
+ return {
481
+ content: [{ type: 'text', text: { error: true, message: "Unknown tool: #{tool_name}" }.to_json }],
482
+ isError: true
483
+ }
484
+ end
485
+
486
+ tool[:handler].call(arguments)
487
+ end
488
+
489
+ def handle_ping(req)
490
+ {}
491
+ end
492
+
493
+ def process_request(req)
494
+ method = req['method']
495
+ id = req['id']
496
+
497
+ result = case method
498
+ when 'initialize' then handle_initialize(req)
499
+ when 'ping' then handle_ping(req)
500
+ when 'tools/list' then handle_tools_list(req)
501
+ when 'tools/call' then handle_tools_call(req)
502
+ when 'notifications/initialized' then nil # 通知,无需响应
503
+ else
504
+ return {
505
+ jsonrpc: '2.0',
506
+ id: id,
507
+ error: { code: -32601, message: "Method not found: #{method}" }
508
+ }
509
+ end
510
+
511
+ return nil if result.nil? && id.nil? # 通知,无响应
512
+
513
+ { jsonrpc: '2.0', id: id, result: result }
514
+ rescue StandardError => e
515
+ { jsonrpc: '2.0', id: id, error: { code: -32603, message: e.message } }
516
+ end
517
+
518
+ # 主循环:逐行读取 STDIN,解析 JSON-RPC,输出到 STDOUT
519
+ $stdout.sync = true
520
+ $stdin.each_line do |line|
521
+ line = line.strip
522
+ next if line.empty?
523
+
524
+ begin
525
+ req = JSON.parse(line)
526
+ rescue JSON::ParserError
527
+ # 单行可能包含多个 JSON-RPC 消息(批量),尝试解析为数组
528
+ begin
529
+ batch = JSON.parse("[#{line}]")
530
+ if batch.is_a?(Array)
531
+ results = batch.map do |item|
532
+ begin
533
+ req = JSON.parse(item)
534
+ process_request(req)
535
+ rescue JSON::ParserError
536
+ { jsonrpc: '2.0', id: nil, error: { code: -32700, message: 'Parse error' } }
537
+ end
538
+ end.compact
539
+ puts results.map(&:to_json).join("\n") unless results.empty?
540
+ next
541
+ end
542
+ rescue JSON::ParserError
543
+ # fall through
544
+ end
545
+ puts({ jsonrpc: '2.0', id: nil, error: { code: -32700, message: 'Parse error' } }.to_json)
546
+ next
547
+ end
548
+
549
+ response = process_request(req)
550
+ puts response.to_json if response
551
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IceJade
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ice-jade
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frampt
@@ -24,6 +24,7 @@ files:
24
24
  - README.md
25
25
  - bin/cradle
26
26
  - bin/cradle-instance
27
+ - bin/mcp_server.rb
27
28
  - config/cradle/poster_test.yml
28
29
  - examples/comparison_getter.rb
29
30
  - examples/comparison_poster.rb