-
Notifications
You must be signed in to change notification settings - Fork 4
Testing with RSpec
This gem provides custom RSpec matchers and helpers for testing your MCP handlers. These matchers validate response structures and content, making it easy to write comprehensive tests for your tools, prompts, and resources.
Add the RSpec configuration to your spec/spec_helper.rb or spec/rails_helper.rb:
require "model_context_protocol/rspec"
# Configure RSpec to include MCP matchers globally
ModelContextProtocol::RSpec.configure!This will make all MCP matchers available in your specs. Additionally, helper methods (call_mcp_tool, call_mcp_prompt, call_mcp_resource) are automatically included for specs with type: :mcp metadata or located in the spec/mcp/ directory.
The following helper methods simplify calling MCP handlers in tests:
| Helper | Description |
|---|---|
call_mcp_tool(tool_class, arguments = {}, context = {}) |
Call a tool class with arguments and optional context |
call_mcp_prompt(prompt_class, arguments = {}, context = {}) |
Call a prompt class with arguments and optional context |
call_mcp_resource(resource_class, context = {}) |
Call a resource class with optional context |
| Matcher | Description |
|---|---|
be_valid_mcp_class |
Validates a class is a properly defined MCP handler |
be_valid_mcp_class(:tool) |
Validates a class is a properly defined MCP tool |
be_valid_mcp_class(:prompt) |
Validates a class is a properly defined MCP prompt |
be_valid_mcp_class(:resource) |
Validates a class is a properly defined MCP resource |
be_valid_mcp_class(:resource_template) |
Validates a class is a properly defined MCP resource template |
| Matcher | Description |
|---|---|
be_valid_mcp_tool_response |
Validates response has correct tool response structure |
have_structured_content(hash) |
Validates response contains matching structured content |
have_text_content(text_or_regex) |
Validates response contains matching text content |
have_image_content |
Validates response contains image content |
have_image_content(mime_type: "image/png") |
Validates response contains image with specific mime type |
have_audio_content |
Validates response contains audio content |
have_audio_content(mime_type: "audio/mpeg") |
Validates response contains audio with specific mime type |
have_embedded_resource_content |
Validates response contains embedded resource |
have_embedded_resource_content(uri: "...", mime_type: "...") |
Validates embedded resource with constraints |
have_resource_link_content |
Validates response contains resource link |
have_resource_link_content(uri: "...", name: "...") |
Validates resource link with constraints |
be_mcp_error_response |
Validates response is an error response |
be_mcp_error_response(text_or_regex) |
Validates error response with matching message |
| Matcher | Description |
|---|---|
be_valid_mcp_prompt_response |
Validates response has correct prompt response structure |
have_message_with_role(role) |
Validates response has message with specified role |
have_message_with_role(role).containing(text_or_regex) |
Validates message with role contains matching content |
have_message_count(n) |
Validates response has exactly n messages |
| Matcher | Description |
|---|---|
be_valid_mcp_resource_response |
Validates response has correct resource response structure |
have_resource_text(text_or_regex) |
Validates resource contains matching text content |
have_resource_blob |
Validates resource contains binary blob content |
have_resource_blob(base64_string) |
Validates resource contains specific blob content |
have_resource_mime_type(type_or_regex) |
Validates resource has matching mime type |
have_resource_annotations(hash) |
Validates resource has matching annotations |
# spec/mcp/tools/weather_tool_spec.rb
RSpec.describe WeatherTool, type: :mcp do
describe "class definition" do
it "is a valid MCP tool" do
expect(WeatherTool).to be_valid_mcp_class(:tool)
end
end
describe "#call" do
context "with valid location" do
it "returns a valid tool response" do
response = call_mcp_tool(WeatherTool, {location: "New York"})
expect(response).to be_valid_mcp_tool_response
end
it "returns structured weather data" do
response = call_mcp_tool(WeatherTool, {location: "New York"})
expect(response).to have_structured_content(
temperature: 72,
conditions: "Sunny"
)
end
it "includes a text summary" do
response = call_mcp_tool(WeatherTool, {location: "New York"})
expect(response).to have_text_content("Current weather")
expect(response).to have_text_content(/\d+ degrees/)
end
end
context "with invalid location" do
it "returns an error response" do
response = call_mcp_tool(WeatherTool, {location: "InvalidPlace"})
expect(response).to be_mcp_error_response
expect(response).to be_mcp_error_response(/location not found/i)
end
end
end
endRSpec.describe ChartGenerator, type: :mcp do
it "generates PNG charts" do
response = call_mcp_tool(ChartGenerator, {format: "png"})
expect(response).to be_valid_mcp_tool_response
expect(response).to have_image_content(mime_type: "image/png")
end
it "generates SVG charts" do
response = call_mcp_tool(ChartGenerator, {format: "svg"})
expect(response).to have_image_content(mime_type: "image/svg+xml")
end
end
RSpec.describe TextToSpeech, type: :mcp do
it "generates audio content" do
response = call_mcp_tool(TextToSpeech, {text: "Hello", format: "mp3"})
expect(response).to have_audio_content(mime_type: "audio/mpeg")
end
end
RSpec.describe DocumentFinder, type: :mcp do
it "returns embedded resources" do
response = call_mcp_tool(DocumentFinder, {query: "readme"})
expect(response).to have_embedded_resource_content(
uri: "file:///docs/README.md",
mime_type: "text/markdown"
)
end
it "returns resource links for large files" do
response = call_mcp_tool(DocumentFinder, {query: "dataset"})
expect(response).to have_resource_link_content(
uri: "file:///data/large.csv",
name: "large-dataset"
)
end
end# spec/mcp/prompts/code_review_prompt_spec.rb
RSpec.describe CodeReviewPrompt, type: :mcp do
describe "class definition" do
it "is a valid MCP prompt" do
expect(CodeReviewPrompt).to be_valid_mcp_class(:prompt)
end
end
describe "#call" do
it "returns a valid prompt response" do
response = call_mcp_prompt(CodeReviewPrompt, {code: "def foo; end"})
expect(response).to be_valid_mcp_prompt_response
end
it "includes the expected message structure" do
response = call_mcp_prompt(CodeReviewPrompt, {code: "def foo; end"})
expect(response).to have_message_count(3)
expect(response).to have_message_with_role("user")
expect(response).to have_message_with_role("assistant")
end
it "includes the code in a user message" do
response = call_mcp_prompt(CodeReviewPrompt, {code: "def hello; end"})
expect(response).to have_message_with_role("user").containing("def hello")
end
it "requests a review in the prompt" do
response = call_mcp_prompt(CodeReviewPrompt, {code: "x = 1"})
expect(response).to have_message_with_role("user").containing(/review/i)
end
end
end# spec/mcp/resources/config_resource_spec.rb
RSpec.describe ConfigResource, type: :mcp do
describe "class definition" do
it "is a valid MCP resource" do
expect(ConfigResource).to be_valid_mcp_class(:resource)
end
end
describe "#call" do
it "returns a valid resource response" do
response = call_mcp_resource(ConfigResource)
expect(response).to be_valid_mcp_resource_response
end
it "returns JSON content" do
response = call_mcp_resource(ConfigResource)
expect(response).to have_resource_text(/"database_url"/)
expect(response).to have_resource_mime_type("application/json")
end
it "includes appropriate annotations" do
response = call_mcp_resource(ConfigResource)
expect(response).to have_resource_annotations(
audience: ["user"],
priority: 0.8
)
end
end
end
# spec/mcp/resources/logo_resource_spec.rb
RSpec.describe LogoResource, type: :mcp do
it "returns binary content" do
response = call_mcp_resource(LogoResource)
expect(response).to be_valid_mcp_resource_response
expect(response).to have_resource_blob
expect(response).to have_resource_mime_type("image/png")
end
endRSpec.describe UserGreetingTool, type: :mcp do
it "uses context for personalization" do
response = call_mcp_tool(
UserGreetingTool,
{message: "Hello"},
{user_id: "123", user_name: "Alice"}
)
expect(response).to have_text_content("Hello, Alice")
end
it "handles missing context gracefully" do
response = call_mcp_tool(UserGreetingTool, {message: "Hello"})
expect(response).to have_text_content("Hello, Guest")
end
endWhen testing controllers that depend on a running MCP server, you need to start and stop the server transport around your examples. If your CI environment doesn't have Redis available, mock the connection pool using mock_redis and connection_pool.
To suppress server logging in tests, configure it in your initializer:
# config/initializers/mcp.rb
ModelContextProtocol::Server.configure_server_logging do |config|
config.logdev = Rails.env.test? ? File::NULL : $stderr
end# spec/controllers/mcp_controller_spec.rb
RSpec.describe McpController do
before do
mock_pool = ConnectionPool.new(size: 1) { MockRedis.new }
allow(ModelContextProtocol::Server::RedisConfig).to receive(:pool).and_return(mock_pool)
ModelContextProtocol::Server.start
end
after do
ModelContextProtocol::Server.shutdown
end
describe "POST /" do
it "returns a successful response" do
post mcp_post_url, params: initialize_params, as: :json
expect(response).to have_http_status(:ok)
end
end
endGetting Started
Server Guide
Handlers
Testing