Skip to content

Commit 394bbb0

Browse files
committed
First version
0 parents  commit 394bbb0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1757
-0
lines changed

.github/workflows/main.yml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Lint and Test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
pull_request:
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
name: Ruby ${{ matrix.ruby }}
14+
strategy:
15+
matrix:
16+
ruby:
17+
- '3.3.1'
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
- name: Set up Ruby
22+
uses: ruby/setup-ruby@v1
23+
with:
24+
ruby-version: ${{ matrix.ruby }}
25+
bundler-cache: true
26+
- name: Run tests
27+
run: bundle exec rubocop
28+
- name: Run tests
29+
run: bundle exec rake test

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/.bundle/
2+
/.yardoc
3+
/_yardoc/
4+
/coverage/
5+
/doc/
6+
/pkg/
7+
/spec/reports/
8+
/tmp/

.rubocop.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
inherit_gem:
2+
rubocop-rails-omakase: rubocop.yml

Gemfile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
5+
# Specify your gem's dependencies in ruby_mcp.gemspec
6+
gemspec
7+
8+
gem "irb"
9+
gem "rake", "~> 13.0"
10+
11+
gem "debug", ">= 1.0.0"
12+
gem "minitest", "~> 5.16"
13+
gem "rubocop-rails-omakase"

Gemfile.lock

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
PATH
2+
remote: .
3+
specs:
4+
ruby_mcp (0.1.0)
5+
zeitwerk (~> 2.7, >= 2.7.2)
6+
7+
GEM
8+
remote: https://rubygems.org/
9+
specs:
10+
activesupport (8.0.2)
11+
base64
12+
benchmark (>= 0.3)
13+
bigdecimal
14+
concurrent-ruby (~> 1.0, >= 1.3.1)
15+
connection_pool (>= 2.2.5)
16+
drb
17+
i18n (>= 1.6, < 2)
18+
logger (>= 1.4.2)
19+
minitest (>= 5.1)
20+
securerandom (>= 0.3)
21+
tzinfo (~> 2.0, >= 2.0.5)
22+
uri (>= 0.13.1)
23+
ast (2.4.3)
24+
base64 (0.2.0)
25+
benchmark (0.4.0)
26+
bigdecimal (3.1.9)
27+
concurrent-ruby (1.3.5)
28+
connection_pool (2.5.0)
29+
date (3.4.1)
30+
debug (1.10.0)
31+
irb (~> 1.10)
32+
reline (>= 0.3.8)
33+
drb (2.2.1)
34+
i18n (1.14.7)
35+
concurrent-ruby (~> 1.0)
36+
io-console (0.8.0)
37+
irb (1.15.1)
38+
pp (>= 0.6.0)
39+
rdoc (>= 4.0.0)
40+
reline (>= 0.4.2)
41+
json (2.10.2)
42+
language_server-protocol (3.17.0.4)
43+
lint_roller (1.1.0)
44+
logger (1.6.6)
45+
minitest (5.25.5)
46+
parallel (1.26.3)
47+
parser (3.3.7.2)
48+
ast (~> 2.4.1)
49+
racc
50+
pp (0.6.2)
51+
prettyprint
52+
prettyprint (0.2.0)
53+
psych (5.2.3)
54+
date
55+
stringio
56+
racc (1.8.1)
57+
rack (3.1.12)
58+
rainbow (3.1.1)
59+
rake (13.2.1)
60+
rdoc (6.12.0)
61+
psych (>= 4.0.0)
62+
regexp_parser (2.10.0)
63+
reline (0.6.0)
64+
io-console (~> 0.5)
65+
rubocop (1.74.0)
66+
json (~> 2.3)
67+
language_server-protocol (~> 3.17.0.2)
68+
lint_roller (~> 1.1.0)
69+
parallel (~> 1.10)
70+
parser (>= 3.3.0.2)
71+
rainbow (>= 2.2.2, < 4.0)
72+
regexp_parser (>= 2.9.3, < 3.0)
73+
rubocop-ast (>= 1.38.0, < 2.0)
74+
ruby-progressbar (~> 1.7)
75+
unicode-display_width (>= 2.4.0, < 4.0)
76+
rubocop-ast (1.41.0)
77+
parser (>= 3.3.7.2)
78+
rubocop-performance (1.24.0)
79+
lint_roller (~> 1.1)
80+
rubocop (>= 1.72.1, < 2.0)
81+
rubocop-ast (>= 1.38.0, < 2.0)
82+
rubocop-rails (2.30.3)
83+
activesupport (>= 4.2.0)
84+
lint_roller (~> 1.1)
85+
rack (>= 1.1)
86+
rubocop (>= 1.72.1, < 2.0)
87+
rubocop-ast (>= 1.38.0, < 2.0)
88+
rubocop-rails-omakase (1.1.0)
89+
rubocop (>= 1.72)
90+
rubocop-performance (>= 1.24)
91+
rubocop-rails (>= 2.30)
92+
ruby-progressbar (1.13.0)
93+
securerandom (0.4.1)
94+
stringio (3.1.5)
95+
tzinfo (2.0.6)
96+
concurrent-ruby (~> 1.0)
97+
unicode-display_width (3.1.4)
98+
unicode-emoji (~> 4.0, >= 4.0.4)
99+
unicode-emoji (4.0.4)
100+
uri (1.0.3)
101+
zeitwerk (2.7.2)
102+
103+
PLATFORMS
104+
arm64-darwin-23
105+
ruby
106+
107+
DEPENDENCIES
108+
debug (>= 1.0.0)
109+
irb
110+
minitest (~> 5.16)
111+
rake (~> 13.0)
112+
rubocop-rails-omakase
113+
ruby_mcp!
114+
115+
BUNDLED WITH
116+
2.6.6

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Niklas Häusele
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# RubyMCP - Build a MCP Server with Ruby
2+
3+
A low-level Model-Context-Protocol implementation for Ruby. Supports [prompts](https://spec.modelcontextprotocol.io/specification/2025-03-26/server/prompts/) and [completions](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/completion/#protocol-messages).
4+
5+
```ruby
6+
server = RubyMCP::Server.new
7+
8+
server.add_prompt(
9+
name: "ruby_example",
10+
description: "Example usage of a method",
11+
arguments: [
12+
{
13+
name: "snippet",
14+
description: "small ruby snippet",
15+
required: true,
16+
completions: ->(*) { [ 'String#split', 'Array#join', 'tally', 'unpack' ] }
17+
}
18+
],
19+
result: ->(snippet:) {
20+
{
21+
description: "Explain '#{snippet}'",
22+
messages: [
23+
{
24+
role: "user",
25+
content: {
26+
type: "text",
27+
text: <<~TXT
28+
You're a coding assistant in the editor zed.
29+
You give one practical example for the given ruby method.
30+
Only answer with a single code snippet and one line of explanation.
31+
For example:
32+
INPUT: '''String#split'''
33+
OUTPUT: 'abc'.split('') # ['a', 'b', 'c']\n Splits the string"
34+
TXT
35+
}
36+
},
37+
{
38+
role: "user",
39+
content: {
40+
type: "text",
41+
text: "INPUT: '''#{snippet}'''"
42+
}
43+
}
44+
]
45+
}
46+
},
47+
)
48+
49+
transport = RubyMCP::Transport::Stdio.new
50+
server.connect(transport)
51+
```
52+
53+
## Logging
54+
55+
RubyMCP supports [mcp logging](https://spec.modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging/). Each logging level has a corresponding method prefixed with `send_` and suffixed with `_log_message`.
56+
57+
- **Debug Level:**
58+
```ruby
59+
server.send_debug_log_message({ text: "Debug information" })
60+
```
61+
62+
- **Info Level:**
63+
```ruby
64+
server.send_info_log_message({ text: "Informational message" })
65+
```
66+
67+
A MCP client can configure change the log severity or it can be set during server creation. The default is "info".
68+
69+
```ruby
70+
server = Server.new(logging_verbosity: "debug")
71+
```

Rakefile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
require "bundler/gem_tasks"
4+
require "minitest/test_task"
5+
6+
Minitest::TestTask.create
7+
8+
task default: :test

bin/console

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "bundler/setup"
5+
require "ruby_mcp"
6+
7+
# You can add fixtures and/or initialization code here to make experimenting
8+
# with your gem easier. You can also use a different console, if you like.
9+
10+
require "irb"
11+
IRB.start(__FILE__)

bin/setup

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
set -vx
5+
6+
bundle install
7+
8+
# Do any other automated setup that you need to do here

exe/ruby_mcp

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env ruby
2+
3+
require "bundler/setup"
4+
require "ruby_mcp"
5+
6+
server = RubyMCP::Server.new
7+
8+
if server_impl = ARGV[0]
9+
server.instance_eval(File.read(server_impl))
10+
else
11+
RubyMCP.logger.info("No implementation given, starting default server")
12+
end
13+
14+
transport = RubyMCP::Transport::Stdio.new
15+
server.connect(transport)

lib/ruby_mcp.rb

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
require "json"
2+
require "securerandom"
3+
require "logger"
4+
require "zeitwerk"
5+
6+
loader = Zeitwerk::Loader.for_gem
7+
loader.inflector.inflect("ruby_mcp" => "RubyMCP")
8+
loader.setup
9+
10+
module RubyMCP
11+
def self.logger
12+
@logger ||= ::Logger.new($stderr).tap do |log|
13+
log.formatter = proc do |severity, datetime, progname, msg|
14+
"[RubyMCP] #{severity[0]}, #{msg}\n"
15+
end
16+
end
17+
end
18+
end
19+
20+
loader.eager_load

lib/ruby_mcp/capabilities/logging.rb

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module RubyMCP::Capabilities::Logging
2+
LEVELS = {
3+
"debug" => 0,
4+
"info" => 1,
5+
"notice" => 2,
6+
"warning" => 3,
7+
"error" => 4,
8+
"critical" => 5,
9+
"alert" => 6,
10+
"emergency" => 7
11+
}
12+
13+
def logging_verbosity
14+
@level ||= @default_logging_verbosity
15+
end
16+
17+
def logging_verbosity=(level)
18+
@level = level
19+
end
20+
21+
LEVELS.each do |name, value|
22+
define_method("send_#{name}_log_message") do |msg|
23+
send_message(
24+
jsonrpc: "2.0", method: "notifications/message", params: msg.merge(level: name)) if value <= LEVELS[logging_verbosity]
25+
end
26+
end
27+
end

lib/ruby_mcp/handlers.rb

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module RubyMCP
2+
class Handlers
3+
def self.parse(json)
4+
parsed = JSON.parse(json)
5+
case parsed.fetch("method")
6+
when "initialize"
7+
Initialize
8+
when "notifications/initialized"
9+
NotificationsInitialized
10+
when "ping"
11+
Ping
12+
when "prompts/get"
13+
PromptsGet
14+
when "prompts/list"
15+
PromptsList
16+
when "completion/complete"
17+
CompletionComplete
18+
when "resources/list"
19+
ResourcesList
20+
when "resources/read"
21+
ResourcesRead
22+
when "logging/setLevel"
23+
LoggingSetLevel
24+
end.new
25+
end
26+
end
27+
end

0 commit comments

Comments
 (0)