Skip to content

Commit 5a1cb8c

Browse files
committed
Add support for multiple results and formats
1 parent b10c5d9 commit 5a1cb8c

File tree

5 files changed

+253
-7
lines changed

5 files changed

+253
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## [Unreleased]
22

3+
- Add support for multi-result responses of any formats
4+
35
## [0.1.0] - 2025-05-26
46

57
- Initial release

Gemfile.lock

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
PATH
2+
remote: .
3+
specs:
4+
tiny_mcp (0.2.0)
5+
6+
GEM
7+
remote: https://rubygems.org/
8+
specs:
9+
date (3.4.1)
10+
erb (5.0.1)
11+
io-console (0.8.0)
12+
irb (1.15.2)
13+
pp (>= 0.6.0)
14+
rdoc (>= 4.0.0)
15+
reline (>= 0.4.2)
16+
minitest (5.25.5)
17+
pp (0.6.2)
18+
prettyprint
19+
prettyprint (0.2.0)
20+
psych (5.2.6)
21+
date
22+
stringio
23+
rake (13.3.0)
24+
rdoc (6.14.0)
25+
erb
26+
psych (>= 4.0.0)
27+
reline (0.6.1)
28+
io-console (~> 0.5)
29+
stringio (3.1.7)
30+
31+
PLATFORMS
32+
arm64-darwin-24
33+
ruby
34+
35+
DEPENDENCIES
36+
irb
37+
minitest (~> 5.16)
38+
rake (~> 13.0)
39+
tiny_mcp!
40+
41+
BUNDLED WITH
42+
2.6.9

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class TimeTool < TinyMCP::Tool
3434
opt :timezone, :string, 'Timezone name'
3535

3636
def call(timezone: 'UTC')
37-
Time.now.getlocal(timezone).to_s
37+
Time.now.getlocal(timezone)
3838
end
3939
end
4040

@@ -56,6 +56,40 @@ claude mcp add my-mcp bin/mcp
5656

5757
The server reads JSON-RPC requests from stdin and writes responses to stdout.
5858

59+
## Multiple results and different formats
60+
61+
By default TinyMCP assumes you're returning `text` from your call function. If you want to return image, audio, or a bunch of different results, wrap your return value in an array, and TinyMCP will treat your return value as the whole `content` body.
62+
63+
Don't forget that binary data such as images and audio needs to be Base64-encoded.
64+
65+
```ruby
66+
require 'base64'
67+
68+
class MultiModalTool < TinyMCP::Tool
69+
name 'get_different_formats'
70+
desc 'Get results in different formats'
71+
72+
def call
73+
[
74+
{
75+
type: 'text',
76+
data: 'This is a text response'
77+
},
78+
{
79+
type: 'image',
80+
mimeType: 'image/png',
81+
data: Base64.strict_encode64(File.read('image.png', 'rb'))
82+
},
83+
{
84+
type: 'audio',
85+
mimeType: 'audio/mpeg',
86+
data: Base64.strict_encode64(File.read('audio.mp3', 'rb'))
87+
}
88+
]
89+
end
90+
end
91+
```
92+
5993
## Development
6094

6195
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

lib/tiny_mcp.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,17 @@ def handle_tool_call(request)
108108
tool = @tools.find { _1.class.mcp.name == name }
109109

110110
if !tool
111-
return error_for(request, :invalid_params, "Unknown_tool: #{name}")
111+
return error_for(request, :invalid_params, "Unknown tool: #{name}")
112112
end
113113

114114
args = request.dig('params', 'arguments')&.transform_keys(&:to_sym)
115115

116116
begin
117117
result = tool.call(**args)
118-
# TODO: Support other content types (ask claude code which ones).
119-
response_for(request, content: [{ type: 'text', text: result.to_s }])
118+
119+
result.is_a?(Array) ?
120+
response_for(request, content: result) :
121+
response_for(request, content: [{ type: 'text', text: result.to_s }])
120122
rescue => e
121123
error_for(request, :internal, e.full_message(highlight: false))
122124
end

test/test_tiny_mcp.rb renamed to test/tiny_mcp_test.rb

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require 'test_helper'
44
require 'stringio'
55

6-
class TestTinyMCP < Minitest::Test
6+
class TinyMCPTest < Minitest::Test
77
def test_that_it_has_a_version_number
88
refute_nil ::TinyMCP::VERSION
99
end
@@ -274,7 +274,7 @@ def test_tools_call_nonexistent_tool
274274

275275
assert response[:error]
276276
assert_equal(-32602, response[:error][:code])
277-
assert_match(/Unknown_tool: nonexistent/, response[:error][:message])
277+
assert_match(/Unknown tool: nonexistent/, response[:error][:message])
278278
end
279279

280280
def test_tools_call_with_error
@@ -337,7 +337,8 @@ def test_error_types
337337

338338
def test_error_with_custom_message
339339
server = TinyMCP::Server.new
340-
error = server.send(:error_for, { 'id' => 1 }, :internal, 'Custom error message')
340+
error =
341+
server.send(:error_for, { 'id' => 1 }, :internal, 'Custom error message')
341342

342343
assert_equal(-32603, error[:error][:code])
343344
assert_equal 'Custom error message', error[:error][:message]
@@ -501,4 +502,169 @@ def call(test_param:)
501502
response = server.send(:handle_request, request)
502503
assert_equal 'Received: value', response[:result][:content][0][:text]
503504
end
505+
506+
# Multi-modal Content Tests
507+
def test_tool_returning_array_of_content_items
508+
multi_content_tool_class = Class.new(TinyMCP::Tool) do
509+
name 'multi_content'
510+
desc 'Returns multiple content items'
511+
arg :content_type, :string, 'Type of content to return'
512+
513+
def call(content_type:)
514+
case content_type
515+
when 'multiple_text'
516+
[
517+
{ type: 'text', text: 'First text item' },
518+
{ type: 'text', text: 'Second text item' },
519+
{ type: 'text', text: 'Third text item' }
520+
]
521+
when 'mixed'
522+
[
523+
{ type: 'text', text: 'Some text content' },
524+
{ type: 'image',
525+
data: 'base64-encoded-image-data',
526+
mimeType: 'image/png' },
527+
{ type: 'text', text: 'More text after image' }
528+
]
529+
end
530+
end
531+
end
532+
533+
server = TinyMCP::Server.new(multi_content_tool_class)
534+
535+
# Test multiple text content
536+
request = {
537+
'jsonrpc' => '2.0',
538+
'id' => 1,
539+
'method' => 'tools/call',
540+
'params' => {
541+
'name' => 'multi_content',
542+
'arguments' => { 'content_type' => 'multiple_text' }
543+
}
544+
}
545+
546+
response = server.send(:handle_request, request)
547+
content = response[:result][:content]
548+
549+
assert_equal 3, content.length
550+
assert_equal 'First text item', content[0][:text]
551+
assert_equal 'Second text item', content[1][:text]
552+
assert_equal 'Third text item', content[2][:text]
553+
content.each { |item| assert_equal 'text', item[:type] }
554+
end
555+
556+
def test_tool_returning_mixed_content_types
557+
mixed_tool_class = Class.new(TinyMCP::Tool) do
558+
name 'mixed_content'
559+
desc 'Returns mixed content types'
560+
561+
def call
562+
[
563+
{ type: 'text', text: 'Here is some text' },
564+
{ type: 'image',
565+
mimeType: 'image/png',
566+
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42' \
567+
'mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==' },
568+
{ type: 'text', text: 'And more text after the image' },
569+
{ type: 'resource',
570+
uri: 'file:///path/to/resource.txt',
571+
text: 'Resource reference' }
572+
]
573+
end
574+
end
575+
576+
server = TinyMCP::Server.new(mixed_tool_class)
577+
request = {
578+
'jsonrpc' => '2.0',
579+
'id' => 1,
580+
'method' => 'tools/call',
581+
'params' => {
582+
'name' => 'mixed_content',
583+
'arguments' => {}
584+
}
585+
}
586+
587+
response = server.send(:handle_request, request)
588+
content = response[:result][:content]
589+
590+
assert_equal 4, content.length
591+
592+
# Check text content
593+
assert_equal 'text', content[0][:type]
594+
assert_equal 'Here is some text', content[0][:text]
595+
596+
# Check image content
597+
assert_equal 'image', content[1][:type]
598+
assert_equal 'image/png', content[1][:mimeType]
599+
assert_equal 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42' \
600+
'mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
601+
content[1][:data]
602+
603+
# Check second text content
604+
assert_equal 'text', content[2][:type]
605+
assert_equal 'And more text after the image', content[2][:text]
606+
607+
# Check resource content
608+
assert_equal 'resource', content[3][:type]
609+
assert_equal 'file:///path/to/resource.txt', content[3][:uri]
610+
assert_equal 'Resource reference', content[3][:text]
611+
end
612+
613+
def test_tool_returning_empty_array
614+
empty_tool_class = Class.new(TinyMCP::Tool) do
615+
name 'empty_content'
616+
desc 'Returns empty content array'
617+
618+
def call
619+
[]
620+
end
621+
end
622+
623+
server = TinyMCP::Server.new(empty_tool_class)
624+
request = {
625+
'jsonrpc' => '2.0',
626+
'id' => 1,
627+
'method' => 'tools/call',
628+
'params' => {
629+
'name' => 'empty_content',
630+
'arguments' => {}
631+
}
632+
}
633+
634+
response = server.send(:handle_request, request)
635+
content = response[:result][:content]
636+
637+
assert_equal [], content
638+
assert_equal 0, content.length
639+
end
640+
641+
def test_tool_returning_single_item_array
642+
single_tool_class = Class.new(TinyMCP::Tool) do
643+
name 'single_content'
644+
desc 'Returns single content item in array'
645+
arg :message, :string, 'Message to return'
646+
647+
def call(message:)
648+
[{ type: 'text', text: message }]
649+
end
650+
end
651+
652+
server = TinyMCP::Server.new(single_tool_class)
653+
request = {
654+
'jsonrpc' => '2.0',
655+
'id' => 1,
656+
'method' => 'tools/call',
657+
'params' => {
658+
'name' => 'single_content',
659+
'arguments' => { 'message' => 'Single item message' }
660+
}
661+
}
662+
663+
response = server.send(:handle_request, request)
664+
content = response[:result][:content]
665+
666+
assert_equal 1, content.length
667+
assert_equal 'text', content[0][:type]
668+
assert_equal 'Single item message', content[0][:text]
669+
end
504670
end

0 commit comments

Comments
 (0)