Skip to content

Commit c88223d

Browse files
committed
update
1 parent bfd14d6 commit c88223d

9 files changed

Lines changed: 97 additions & 74 deletions

File tree

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
stack-service-base (0.0.64)
4+
stack-service-base (0.0.66)
55
async
66
debug
77
dotenv

lib/stack-service-base/examples/mcp_config.ru

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ SERVICES = {
1111
}
1212
}
1313

14-
require 'stack-service-base/mcp_helper'
14+
require 'stack-service-base/mcp/mcp_helper'
1515
helpers McpHelper
1616

1717
Tool :search do
1818
description 'Search for a term in the database'
1919
input query: { type: "string", description: "Term to search for", required: true }
20-
execute do |inputs|
20+
call do |inputs|
2121
query = inputs[:query]
2222
{ results: [{id:"doc-1",title:"...",url:"..."}] }
2323
end
@@ -26,7 +26,7 @@ end
2626
Tool :fetch do
2727
description 'Fetch a resource from the database'
2828
input resource_id: { type: "string", description: "Resource ID to fetch", required: true }
29-
execute do |inputs|
29+
call do |inputs|
3030
id = inputs[:id]
3131
{ id: "doc-1", title: "...", text: "full text...", url: "https://example.com/doc", metadata: { source: "vector_store" } }
3232
end
@@ -35,10 +35,10 @@ end
3535
Tool :service_status do
3636
description 'Check current status of a service'
3737
input service_name: { type: "string", description: "Service name to inspect", required: true }
38-
execute do |inputs|
38+
call do |inputs|
3939
service_name = inputs[:service_name]
4040
service = SERVICES[service_name]
41-
rpc_error!(404, "Unknown service #{service_name}") unless service
41+
rpc_error!(-32000, "Unknown service #{service_name}") unless service
4242
{
4343
service_name: service_name,
4444
status: service[:status],
@@ -52,10 +52,10 @@ Tool :restart_service do
5252
description 'Restart a service'
5353
input service_name: { type: "string", description: "Service name to restart", required: true },
5454
force: { type: "boolean", default: false, description: "Force restart if graceful fails" }
55-
execute do |inputs|
55+
call do |inputs|
5656
service_name = inputs[:service_name]
5757
service = SERVICES[service_name]
58-
rpc_error!(404, "Unknown service #{service_name}") unless service
58+
rpc_error!(-32000, "Unknown service #{service_name}") unless service
5959

6060
service[:status] = "running"
6161
service[:last_restart] = Time.now
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def self.included(base)
1212
err.body
1313
end
1414

15-
get '/mcp' do
15+
get '/mcp' do
1616
content_type :json
1717
MCP_PROCESSOR.root_endpoint
1818
end

lib/stack-service-base/mcp_processor.rb renamed to lib/stack-service-base/mcp/mcp_processor.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def call_tool(params)
115115
name = params["name"]
116116
arguments = params["arguments"] || {}
117117
tool = ToolRegistry.fetch(name) || rpc_error!(-32601, "Unknown tool #{name}")
118-
response = tool.call(arguments)
118+
response = tool.call_tool(arguments)
119119
{
120120
content: [
121121
{ "type": "text", "text": response.is_a?(String) ? response : response.to_json }

lib/stack-service-base/mcp_tool_registry.rb renamed to lib/stack-service-base/mcp/mcp_tool_registry.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def input_schema
3030
properties[field] = cfg.transform_keys(&:to_s)
3131
end
3232

33-
{ type: "object", properties: properties, required: required }
33+
{ type: 'object', properties: properties, required: required }
3434
end
3535

36-
def execute(&block) = @executor = block
36+
def call(&block) = @executor = block
3737

3838
def to_h
3939
{
@@ -43,7 +43,7 @@ def to_h
4343
}
4444
end
4545

46-
def call(arguments)
46+
def call_tool(arguments)
4747
raise JsonRpcError.new(code: 500, message: "Tool #{name} missing executor") unless @executor
4848

4949
ExecutionContext.new.instance_exec(symbolize_keys(arguments || {}), &@executor)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require 'json'
2+
3+
module Rack
4+
module Test
5+
module McpProtocol
6+
def rpc_request(payload)
7+
post '/mcp', JSON.dump(payload), { 'CONTENT_TYPE' => 'application/json' }
8+
expect(last_response.status).to eq(200)
9+
parsed_response_body
10+
end
11+
12+
def parsed_response_body
13+
body = +''
14+
last_response.each { |chunk| body << chunk.to_s }
15+
data_lines = body.lines.select { |line| line.start_with?('data:') }
16+
payload_line = data_lines.reverse.find { |l| l !~ /ping/i } || data_lines.last
17+
payload = (payload_line || body).sub(/\Adata:\s*/, '').sub(/\n\z/, '')
18+
JSON.parse(payload, symbolize_names: true)
19+
end
20+
21+
def mcp_list_tools
22+
req_id = next_id
23+
response = rpc_request(id: req_id, method: 'tools/list', params: {})
24+
expect(response[:id]).to eq(req_id)
25+
response.dig(:result, :tools)
26+
end
27+
28+
def mcp_call_tool(name:, arguments:)
29+
response = mcp_call_tool_raw(name: name, arguments: arguments)
30+
tool_results = response.dig(:result, :content)
31+
expect(tool_results[0][:type]).to eq('text')
32+
JSON.parse(tool_results[0][:text], symbolize_names: true)
33+
end
34+
35+
def mcp_call_tool_raw(name:, arguments:)
36+
rpc_request(
37+
id: next_id,
38+
method: 'tools/call',
39+
params: { 'name' => name, 'arguments' => arguments }
40+
)
41+
end
42+
43+
private
44+
45+
def next_id
46+
@next_id ||= -1
47+
@next_id += 1
48+
end
49+
end
50+
end
51+
end
52+

lib/stack-service-base/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module StackServiceBase
2-
VERSION = '0.0.65'
2+
VERSION = '0.0.66'
33
end

spec/integration_spec.rb

Lines changed: 29 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,98 +12,67 @@
1212
let(:headers) { { 'CONTENT_TYPE' => 'application/json' } }
1313

1414
it 'initializes the client session' do
15+
req_id = next_id
1516
response = rpc_request(
16-
id: 10,
17+
id: req_id,
1718
method: 'initialize',
1819
params: {}
1920
)
2021

21-
expect(response['id']).to eq(10)
22-
result = response['result']
22+
expect(response[:id]).to eq(req_id)
23+
result = response[:result]
2324

24-
expect(result['serverInfo']).to include(
25-
'name' => 'mcp-server',
26-
'title' => 'MCP Server',
27-
'version' => '1.0.0'
28-
)
25+
expect(result[:serverInfo]).to include(
26+
name: 'mcp-server',
27+
title: 'MCP Server',
28+
version: '1.0.0'
29+
)
2930

30-
expect(result['protocolVersion']).to eq(McpProcessor::PROTOCOL_VERSION)
31-
expect(result['capabilities']).to include(
32-
'logging' => {},
33-
'prompts' => { 'listChanged' => false },
34-
'resources' => { 'listChanged' => false },
35-
'tools' => { 'listChanged' => false }
36-
)
31+
expect(result[:protocolVersion]).to eq(McpProcessor::PROTOCOL_VERSION)
32+
expect(result[:capabilities]).to include(
33+
logging: {},
34+
prompts: { listChanged: false },
35+
resources: { listChanged: false },
36+
tools: { listChanged: false }
37+
)
3738
end
3839

3940
it 'lists tools via GET /mcp' do
4041
get '/mcp'
4142
expect(last_response.status).to eq(200)
4243
body = parsed_response_body
43-
expect(body['id']).to be_nil
44-
expect(body.dig('result', 'tools').map { |tool| tool['name'] }).to include('service_status', 'restart_service')
44+
expect(body[:id]).to be_nil
45+
expect(body.dig(:result, :tools).map { |tool| tool[:name] }).to include('service_status', 'restart_service')
4546
end
4647

4748
it 'lists tools via RPC' do
48-
body = rpc_request(id: 1, method: 'tools/list', params: {})
49-
expect(body['id']).to eq(1)
50-
names = body.dig('result', 'tools').map { |tool| tool['name'] }
49+
names = mcp_list_tools.map { |tool| tool[:name] }
5150
expect(names).to include('service_status', 'restart_service')
5251
end
5352

5453
it 'executes service_status successfully' do
55-
response = rpc_request(
56-
id: 2,
57-
method: 'tools/call',
58-
params: { 'name' => 'service_status', 'arguments' => { 'service_name' => 'database-backend' } }
59-
)
60-
tool_results = response.dig('result', 'content')
61-
expect(tool_results[0]['type']).to eq('text')
62-
tool_result = JSON.parse tool_results[0]['text']
63-
expect(tool_result['service_name']).to eq('database-backend')
64-
expect(tool_result['status']).to eq('running')
54+
tool_result = mcp_call_tool(name: 'service_status', arguments: { service_name: 'database-backend' })
55+
expect(tool_result[:service_name]).to eq('database-backend')
56+
expect(tool_result[:status]).to eq('running')
6557
end
6658

6759
it 'returns tool error when service is missing' do
68-
response = rpc_request(
69-
id: 3,
70-
method: 'tools/call',
71-
params: { 'name' => 'service_status', 'arguments' => { 'service_name' => 'ghost' } }
72-
)
73-
expect(response['result']).to be_nil
74-
expect(response.dig('error', 'code')).to eq(404)
75-
expect(response.dig('error', 'message')).to include('Unknown service ghost')
60+
response = mcp_call_tool_raw(name: 'service_status', arguments: { service_name: 'ghost' })
61+
expect(response[:result]).to be_nil
62+
expect(response.dig(:error, :code)).to eq(-32000)
63+
expect(response.dig(:error, :message)).to include('Unknown service ghost')
7664
end
7765

7866
it 'returns error for unknown tools' do
79-
response = rpc_request(
80-
id: 4,
81-
method: 'tools/call',
82-
params: { 'name' => 'unknown', 'arguments' => {} }
83-
)
84-
expect(response.dig('error', 'code')).to eq(-32601)
67+
response = mcp_call_tool_raw(name: 'unknown', arguments: {})
68+
expect(response.dig(:error, :code)).to eq(-32601)
8569
end
8670

8771
it 'handles malformed JSON payloads' do
8872
post '/mcp', '{invalid', headers
8973
expect(last_response.status).to eq(400)
9074
body = parsed_response_body
91-
expect(body.dig('error', 'code')).to eq(-32700)
92-
end
93-
94-
def rpc_request(payload)
95-
post '/mcp', JSON.dump(payload), headers
96-
expect(last_response.status).to eq(200)
97-
parsed_response_body
98-
end
99-
100-
def parsed_response_body
101-
body = +''
102-
last_response.each { |chunk| body << chunk.to_s }
103-
data_lines = body.lines.select { |line| line.start_with?('data:') }
104-
payload_line = data_lines.reverse.find { |l| l !~ /ping/i } || data_lines.last
105-
payload = (payload_line || body).sub(/\Adata:\s*/, '').sub(/\n\z/, '')
106-
JSON.parse(payload)
75+
expect(body.dig(:error, :code)).to eq(-32700)
10776
end
10877
end
10978
end

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require "rspec/snapshot"
1212
require 'testcontainers'
1313
require 'simplecov'
14+
require 'stack-service-base/mcp/rack_test_mcp_protocol'
1415
SimpleCov.start
1516

1617
#ENV['DB_URL'] = 'sqlite::memory:'
@@ -25,6 +26,7 @@ def app = RSpec.configuration.app
2526
config.include RSpec::Benchmark::Matchers
2627
config.include RSpec::Snapshot
2728
config.include_context Async::RSpec::Reactor
29+
config.include Rack::Test::McpProtocol
2830
config.add_setting :pg_container
2931
config.add_setting :app
3032

0 commit comments

Comments
 (0)