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 +4 -4
- data/bin/mcp_server.rb +551 -0
- data/lib/ice_jade/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b39e51e5e392d1d9ffd1ee3c991a0e22ea515a03b3ce3f2450642c8b460e595
|
|
4
|
+
data.tar.gz: 960d1cb2bd5f82023ed99de8ceff20dff679356a409086aecf89b0884b3895a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/ice_jade/version.rb
CHANGED
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.
|
|
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
|