rasti-ai 1.1.0 → 1.2.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/.gitignore +2 -1
- data/README.md +151 -3
- data/lib/rasti/ai/mcp/client.rb +68 -0
- data/lib/rasti/ai/mcp/errors.rb +29 -0
- data/lib/rasti/ai/mcp/server.rb +156 -0
- data/lib/rasti/ai/open_ai/assistant.rb +25 -3
- data/lib/rasti/ai/tool.rb +23 -0
- data/lib/rasti/ai/tool_serializer.rb +112 -0
- data/lib/rasti/ai/version.rb +1 -1
- data/log/.gitkeep +0 -0
- data/rasti-ai.gemspec +2 -0
- data/spec/mcp/client_spec.rb +256 -0
- data/spec/mcp/server_spec.rb +372 -0
- data/spec/minitest_helper.rb +2 -0
- data/spec/open_ai/assistant_spec.rb +9 -2
- data/spec/{open_ai/tool_serializer_spec.rb → tool_serializer_spec.rb} +21 -29
- metadata +42 -5
- data/lib/rasti/ai/open_ai/tool_serializer.rb +0 -111
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
require 'minitest_helper'
|
|
2
|
+
|
|
3
|
+
describe Rasti::AI::MCP::Client do
|
|
4
|
+
|
|
5
|
+
let(:mcp_url) { 'https://mcp.server.ai/mcp' }
|
|
6
|
+
|
|
7
|
+
let(:client) { Rasti::AI::MCP::Client.new(url: mcp_url) }
|
|
8
|
+
|
|
9
|
+
let(:client_with_allowed_tools) { Rasti::AI::MCP::Client.new(url: mcp_url, allowed_tools: ['sum_tool']) }
|
|
10
|
+
|
|
11
|
+
def stub_mcp_request(method, params:{}, result:{}, error:nil)
|
|
12
|
+
request_body = {
|
|
13
|
+
jsonrpc: '2.0',
|
|
14
|
+
method: method,
|
|
15
|
+
params: params
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
response_body = {
|
|
19
|
+
jsonrpc: '2.0'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if error
|
|
23
|
+
response_body[:error] = error
|
|
24
|
+
else
|
|
25
|
+
response_body[:result] = result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
stub_request(:post, mcp_url)
|
|
29
|
+
.with(
|
|
30
|
+
body: JSON.dump(request_body),
|
|
31
|
+
headers: {'Content-Type' => 'application/json'}
|
|
32
|
+
)
|
|
33
|
+
.to_return(
|
|
34
|
+
body: JSON.dump(response_body),
|
|
35
|
+
headers: {'Content-Type' => 'application/json'}
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'List tools' do
|
|
40
|
+
|
|
41
|
+
it 'Returns all tools when no allowed_tools' do
|
|
42
|
+
tools = [
|
|
43
|
+
{
|
|
44
|
+
'name' => 'hello_world_tool',
|
|
45
|
+
'description' => 'Hello World'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
'name' => 'sum_tool',
|
|
49
|
+
'description' => 'Sum two numbers'
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
stub_mcp_request 'tools/list', result: {'tools' => tools}
|
|
54
|
+
|
|
55
|
+
result = client.list_tools
|
|
56
|
+
|
|
57
|
+
assert_equal tools, result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'Returns empty list when no tools available' do
|
|
61
|
+
stub_mcp_request 'tools/list', result: {'tools' => []}
|
|
62
|
+
|
|
63
|
+
result = client.list_tools
|
|
64
|
+
|
|
65
|
+
assert_equal [], result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'Filters tools by allowed_tools' do
|
|
69
|
+
tools = [
|
|
70
|
+
{
|
|
71
|
+
'name' => 'hello_world_tool',
|
|
72
|
+
'description' => 'Hello World'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
'name' => 'sum_tool',
|
|
76
|
+
'description' => 'Sum two numbers'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
'name' => 'multiply_tool',
|
|
80
|
+
'description' => 'Multiply'
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
stub_mcp_request 'tools/list', result: {'tools' => tools}
|
|
85
|
+
|
|
86
|
+
result = client_with_allowed_tools.list_tools
|
|
87
|
+
|
|
88
|
+
expected = [
|
|
89
|
+
{
|
|
90
|
+
'name' => 'sum_tool',
|
|
91
|
+
'description' => 'Sum two numbers'
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
assert_equal expected, result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'Returns empty when no tools match allowed_tools' do
|
|
99
|
+
tools = [
|
|
100
|
+
{
|
|
101
|
+
'name' => 'hello_world_tool',
|
|
102
|
+
'description' => 'Hello World'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
'name' => 'multiply_tool',
|
|
106
|
+
'description' => 'Multiply'
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
stub_mcp_request 'tools/list', result: {'tools' => tools}
|
|
111
|
+
|
|
112
|
+
result = client_with_allowed_tools.list_tools
|
|
113
|
+
|
|
114
|
+
assert_equal [], result
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe 'Call tool' do
|
|
120
|
+
|
|
121
|
+
it 'Executes tool with arguments' do
|
|
122
|
+
params = {
|
|
123
|
+
name: 'sum_tool',
|
|
124
|
+
arguments: {
|
|
125
|
+
number_a: 1,
|
|
126
|
+
number_b: 2
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
result_content = {
|
|
131
|
+
'content' => [
|
|
132
|
+
{
|
|
133
|
+
'type' => 'text',
|
|
134
|
+
'text' => '{"result":3.0}'
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
stub_mcp_request 'tools/call', params: params, result: result_content
|
|
140
|
+
|
|
141
|
+
result = client.call_tool 'sum_tool', number_a: 1, number_b: 2
|
|
142
|
+
|
|
143
|
+
assert_equal '{"type":"text","text":"{\"result\":3.0}"}', result
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'Executes tool without arguments' do
|
|
147
|
+
params = {
|
|
148
|
+
name: 'hello_world_tool',
|
|
149
|
+
arguments: {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
result_content = {
|
|
153
|
+
'content' => [
|
|
154
|
+
{
|
|
155
|
+
'type' => 'text',
|
|
156
|
+
'text' => '{"text":"Hello world"}'
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stub_mcp_request 'tools/call', params: params, result: result_content
|
|
162
|
+
|
|
163
|
+
result = client.call_tool 'hello_world_tool'
|
|
164
|
+
|
|
165
|
+
assert_equal '{"type":"text","text":"{\"text\":\"Hello world\"}"}', result
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'Raises error when tool not in allowed_tools' do
|
|
169
|
+
error = assert_raises RuntimeError do
|
|
170
|
+
client_with_allowed_tools.call_tool 'hello_world_tool'
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
assert_equal 'Invalid tool: hello_world_tool', error.message
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'Allows tool call when in allowed_tools' do
|
|
177
|
+
params = {
|
|
178
|
+
name: 'sum_tool',
|
|
179
|
+
arguments: {
|
|
180
|
+
number_a: 5,
|
|
181
|
+
number_b: 3
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
result_content = {
|
|
186
|
+
'content' => [
|
|
187
|
+
{
|
|
188
|
+
'type' => 'text',
|
|
189
|
+
'text' => '{"result":8.0}'
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
stub_mcp_request 'tools/call', params: params, result: result_content
|
|
195
|
+
|
|
196
|
+
result = client_with_allowed_tools.call_tool 'sum_tool', number_a: 5, number_b: 3
|
|
197
|
+
|
|
198
|
+
assert_equal '{"type":"text","text":"{\"result\":8.0}"}', result
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe 'Error handling' do
|
|
204
|
+
|
|
205
|
+
it 'Raises error when MCP returns error' do
|
|
206
|
+
error_response = {
|
|
207
|
+
'code' => -32603,
|
|
208
|
+
'message' => 'Tool not found'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
params = {
|
|
212
|
+
name: 'nonexistent',
|
|
213
|
+
arguments: {}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
stub_mcp_request 'tools/call', params: params, error: error_response
|
|
217
|
+
|
|
218
|
+
error = assert_raises RuntimeError do
|
|
219
|
+
client.call_tool 'nonexistent'
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
assert_equal 'MCP Error: Tool not found', error.message
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'Raises error when result is missing' do
|
|
226
|
+
stub_request(:post, mcp_url)
|
|
227
|
+
.to_return(
|
|
228
|
+
status: 200,
|
|
229
|
+
body: JSON.dump({ jsonrpc: '2.0' }),
|
|
230
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
error = assert_raises RuntimeError do
|
|
234
|
+
client.list_tools
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
assert_equal 'MCP Error: invalid result', error.message
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'Raises error when response is invalid JSON' do
|
|
241
|
+
stub_request(:post, mcp_url)
|
|
242
|
+
.to_return(
|
|
243
|
+
status: 200,
|
|
244
|
+
body: 'invalid json{',
|
|
245
|
+
headers: {'Content-Type' => 'application/json'}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
error = assert_raises JSON::ParserError do
|
|
249
|
+
client.list_tools
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
assert_match /unexpected token/, error.message
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
require 'minitest_helper'
|
|
2
|
+
|
|
3
|
+
describe Rasti::AI::MCP::Server do
|
|
4
|
+
|
|
5
|
+
class HelloWorldTool < Rasti::AI::Tool
|
|
6
|
+
def self.description
|
|
7
|
+
'Hello World'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def execute(form)
|
|
11
|
+
{text: 'Hello world'}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class SumTool < Rasti::AI::Tool
|
|
16
|
+
class Form < Rasti::Form
|
|
17
|
+
attribute :number_a, Rasti::Types::Float
|
|
18
|
+
attribute :number_b, Rasti::Types::Float
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execute(form)
|
|
22
|
+
{result: form.number_a + form.number_b}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class ErrorTool < Rasti::AI::Tool
|
|
27
|
+
def execute(form)
|
|
28
|
+
raise "Unexpected tool error"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
include Rack::Test::Methods
|
|
33
|
+
|
|
34
|
+
let(:app) do
|
|
35
|
+
app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['App response']] }
|
|
36
|
+
Rasti::AI::MCP::Server.new app
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
before do
|
|
40
|
+
Rasti::AI::MCP::Server.clear_tools
|
|
41
|
+
Rasti::AI::MCP::Server.restore_default_configuration
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def post_mcp_request(method, params={})
|
|
45
|
+
request_data = {
|
|
46
|
+
jsonrpc: '2.0',
|
|
47
|
+
id: 1,
|
|
48
|
+
method: method
|
|
49
|
+
}
|
|
50
|
+
request_data[:params] = params unless params.empty?
|
|
51
|
+
|
|
52
|
+
post Rasti::AI::MCP::Server.relative_path, JSON.dump(request_data), CONTENT_TYPE: 'application/json'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def assert_jsonrpc_success(expected_result)
|
|
56
|
+
expected_response = {
|
|
57
|
+
jsonrpc: '2.0',
|
|
58
|
+
id: 1,
|
|
59
|
+
result: expected_result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
assert_equal 200, last_response.status
|
|
63
|
+
assert_equal 'application/json', last_response.content_type
|
|
64
|
+
assert_equal_json JSON.dump(expected_response), last_response.body
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def assert_jsonrpc_error(expected_error)
|
|
68
|
+
expected_response = {
|
|
69
|
+
jsonrpc: '2.0',
|
|
70
|
+
id: 1,
|
|
71
|
+
error: expected_error
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
assert_equal 200, last_response.status
|
|
75
|
+
assert_equal 'application/json', last_response.content_type
|
|
76
|
+
assert_equal_json JSON.dump(expected_response), last_response.body
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe 'Tool registration' do
|
|
80
|
+
|
|
81
|
+
it 'Register tool' do
|
|
82
|
+
Rasti::AI::MCP::Server.register_tool HelloWorldTool.new
|
|
83
|
+
Rasti::AI::MCP::Server.register_tool SumTool.new
|
|
84
|
+
|
|
85
|
+
serializations = Rasti::AI::MCP::Server.tools_serializations
|
|
86
|
+
|
|
87
|
+
expeted_serializations = [
|
|
88
|
+
Rasti::AI::ToolSerializer.serialize(HelloWorldTool),
|
|
89
|
+
Rasti::AI::ToolSerializer.serialize(SumTool)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
assert_equal expeted_serializations, serializations
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'Register duplicate tool raises error' do
|
|
96
|
+
Rasti::AI::MCP::Server.register_tool HelloWorldTool.new
|
|
97
|
+
|
|
98
|
+
error = assert_raises RuntimeError do
|
|
99
|
+
Rasti::AI::MCP::Server.register_tool HelloWorldTool.new
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
assert_equal 'Tool hello_world_tool already exist', error.message
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'Tools serializations empty when no tools' do
|
|
106
|
+
assert_empty Rasti::AI::MCP::Server.tools_serializations
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'Call tool executes registered tool' do
|
|
110
|
+
Rasti::AI::MCP::Server.register_tool HelloWorldTool.new
|
|
111
|
+
|
|
112
|
+
result = Rasti::AI::MCP::Server.call_tool 'hello_world_tool'
|
|
113
|
+
|
|
114
|
+
assert_equal '{"text":"Hello world"}', result
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'Call tool not found raises error' do
|
|
118
|
+
error = assert_raises RuntimeError do
|
|
119
|
+
Rasti::AI::MCP::Server.call_tool 'non_existent', {}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
assert_equal 'Tool non_existent not found', error.message
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe 'Initialize request' do
|
|
128
|
+
|
|
129
|
+
it 'Returns protocol version and capabilities' do
|
|
130
|
+
post_mcp_request 'initialize'
|
|
131
|
+
|
|
132
|
+
expected_result = {
|
|
133
|
+
protocolVersion: '2024-11-05',
|
|
134
|
+
capabilities: {
|
|
135
|
+
tools: {
|
|
136
|
+
list: true,
|
|
137
|
+
call: true
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
serverInfo: {
|
|
141
|
+
name: 'MCP Server',
|
|
142
|
+
version: '1.0.0'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
assert_jsonrpc_success expected_result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'Custom server configuration' do
|
|
150
|
+
Rasti::AI::MCP::Server.configure do |config|
|
|
151
|
+
config.server_name = 'Custom MCP Server'
|
|
152
|
+
config.server_version = '2.0.0'
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
post_mcp_request 'initialize'
|
|
156
|
+
|
|
157
|
+
expected_result = {
|
|
158
|
+
protocolVersion: '2024-11-05',
|
|
159
|
+
capabilities: {
|
|
160
|
+
tools: {
|
|
161
|
+
list: true,
|
|
162
|
+
call: true
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
serverInfo: {
|
|
166
|
+
name: 'Custom MCP Server',
|
|
167
|
+
version: '2.0.0'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
assert_jsonrpc_success expected_result
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe 'Tools list request' do
|
|
177
|
+
|
|
178
|
+
it 'Returns empty list when no tools' do
|
|
179
|
+
post_mcp_request 'tools/list'
|
|
180
|
+
|
|
181
|
+
expected_result = {
|
|
182
|
+
tools: []
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
assert_jsonrpc_success expected_result
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'Returns registered tools' do
|
|
189
|
+
Rasti::AI::MCP::Server.register_tool HelloWorldTool.new
|
|
190
|
+
Rasti::AI::MCP::Server.register_tool SumTool.new
|
|
191
|
+
|
|
192
|
+
post_mcp_request 'tools/list'
|
|
193
|
+
|
|
194
|
+
expected_result = {
|
|
195
|
+
tools: [
|
|
196
|
+
Rasti::AI::ToolSerializer.serialize(HelloWorldTool),
|
|
197
|
+
Rasti::AI::ToolSerializer.serialize(SumTool)
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
assert_jsonrpc_success expected_result
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe 'Tools call request' do
|
|
207
|
+
|
|
208
|
+
before do
|
|
209
|
+
Rasti::AI::MCP::Server.register_tool HelloWorldTool.new
|
|
210
|
+
Rasti::AI::MCP::Server.register_tool SumTool.new
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'Executes tool with arguments' do
|
|
214
|
+
params = {
|
|
215
|
+
name: 'sum_tool',
|
|
216
|
+
arguments: {
|
|
217
|
+
number_a: 1,
|
|
218
|
+
number_b: 2
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
post_mcp_request 'tools/call', params
|
|
223
|
+
|
|
224
|
+
expected_result = {
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: '{"result":3.0}'
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
assert_jsonrpc_success expected_result
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'Executes tool without arguments' do
|
|
237
|
+
params = {
|
|
238
|
+
name: 'hello_world_tool'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
post_mcp_request 'tools/call', params
|
|
242
|
+
|
|
243
|
+
expected_result = {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: 'text',
|
|
247
|
+
text: '{"text":"Hello world"}'
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
assert_jsonrpc_success expected_result
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'Tool call with nonexistent tool returns error' do
|
|
256
|
+
params = {
|
|
257
|
+
name: 'nonexistent'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
post_mcp_request 'tools/call', params
|
|
261
|
+
|
|
262
|
+
expected_error = {
|
|
263
|
+
code: -32603,
|
|
264
|
+
message: 'Tool nonexistent not found'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
assert_jsonrpc_error expected_error
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'Tool execution error returns error response' do
|
|
271
|
+
Rasti::AI::MCP::Server.register_tool ErrorTool.new
|
|
272
|
+
|
|
273
|
+
params = {
|
|
274
|
+
name: 'error_tool'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
post_mcp_request 'tools/call', params
|
|
278
|
+
|
|
279
|
+
expected_error = {
|
|
280
|
+
code: -32603,
|
|
281
|
+
message: 'Unexpected tool error'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
assert_jsonrpc_error expected_error
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
describe 'Error handling' do
|
|
290
|
+
|
|
291
|
+
it 'Method not found' do
|
|
292
|
+
post_mcp_request 'invalid/method'
|
|
293
|
+
|
|
294
|
+
expected_error = {
|
|
295
|
+
code: -32601,
|
|
296
|
+
message: 'Method not found'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
assert_jsonrpc_error expected_error
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it 'Invalid JSON returns 400' do
|
|
303
|
+
post '/mcp', 'invalid json{'
|
|
304
|
+
|
|
305
|
+
assert_equal 400, last_response.status
|
|
306
|
+
assert_equal 'application/json', last_response.content_type
|
|
307
|
+
|
|
308
|
+
json_response = JSON.parse last_response.body
|
|
309
|
+
assert_equal -32700, json_response['error']['code']
|
|
310
|
+
assert_match /unexpected token/, json_response['error']['message']
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'Unhandled exception returns 500' do
|
|
314
|
+
app.stub :handle_initialize, ->(_) { raise 'Unexpected server error' } do
|
|
315
|
+
post_mcp_request 'initialize'
|
|
316
|
+
|
|
317
|
+
expected_response = {
|
|
318
|
+
jsonrpc: '2.0',
|
|
319
|
+
id: nil,
|
|
320
|
+
error: {
|
|
321
|
+
code: -32603,
|
|
322
|
+
message: 'Unexpected server error'
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
assert_equal 500, last_response.status
|
|
327
|
+
assert_equal 'application/json', last_response.content_type
|
|
328
|
+
assert_equal_json JSON.dump(expected_response), last_response.body
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
describe 'Middleware behavior' do
|
|
335
|
+
|
|
336
|
+
it 'Non MCP requests passed to app' do
|
|
337
|
+
get '/other/path'
|
|
338
|
+
|
|
339
|
+
assert_equal 200, last_response.status
|
|
340
|
+
assert_equal 'App response', last_response.body
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'GET requests to MCP path passed to app' do
|
|
344
|
+
get '/mcp'
|
|
345
|
+
|
|
346
|
+
assert_equal 200, last_response.status
|
|
347
|
+
assert_equal 'App response', last_response.body
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it 'POST to different path passed to app' do
|
|
351
|
+
post '/other/path'
|
|
352
|
+
|
|
353
|
+
assert_equal 200, last_response.status
|
|
354
|
+
assert_equal 'App response', last_response.body
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it 'Custom relative path' do
|
|
358
|
+
Rasti::AI::MCP::Server.configure do |config|
|
|
359
|
+
config.relative_path = '/path/too/custom_mcp'
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
post '/mcp'
|
|
363
|
+
assert_equal 200, last_response.status
|
|
364
|
+
assert_equal 'App response', last_response.body
|
|
365
|
+
|
|
366
|
+
post_mcp_request 'tools/list'
|
|
367
|
+
assert_jsonrpc_success tools: []
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
end
|
data/spec/minitest_helper.rb
CHANGED
|
@@ -163,11 +163,18 @@ describe Rasti::AI::OpenAI::Assistant do
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def stub_client_request(role:, content:, response:, tools:[])
|
|
166
|
+
serialized_tools = tools.map do |tool|
|
|
167
|
+
{
|
|
168
|
+
type: 'function',
|
|
169
|
+
function: Rasti::AI::ToolSerializer.serialize(tool.class)
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
166
173
|
client.expect :chat_completions, response do |params|
|
|
167
174
|
last_message = params[:messages].last
|
|
168
175
|
last_message[:role] == role &&
|
|
169
176
|
last_message[:content] == content &&
|
|
170
|
-
params[:tools] ==
|
|
177
|
+
params[:tools] == serialized_tools
|
|
171
178
|
end
|
|
172
179
|
end
|
|
173
180
|
|
|
@@ -295,4 +302,4 @@ describe Rasti::AI::OpenAI::Assistant do
|
|
|
295
302
|
|
|
296
303
|
end
|
|
297
304
|
|
|
298
|
-
end
|
|
305
|
+
end
|