mcp-rb 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 610a73e9025b6400f091beeec7e0e0b9b51e917ba57b068f7132fd2dad3fa207
4
- data.tar.gz: 8a5533dd1abdb1fbdde067facfe0ceeeb50d9fac332ba4ec4c4815a9b8f1a199
3
+ metadata.gz: 131c29522f8eb44f7f613f51d92db433370d99950dcfcd27340ab9917b306dd3
4
+ data.tar.gz: a676110b09851f7018205bc012b09a6f22d1c7730555ba569b9b7ae475c06009
5
5
  SHA512:
6
- metadata.gz: be76a1399e5121d0e0886489630fe7695b9fba3cea55835d6da2d67d5b74d3a714f68c354dbf447e8953f965c0c65e416e12417ee109fe4695f47cc4c940bdc6
7
- data.tar.gz: d0ec9f439c4bc497c264241000269b7c5928167f0623a3a34335e6c28b597c95967b38819051a954f96323e861ea6553d218bc639df7b52697185463da1ac158
6
+ metadata.gz: ebe914ec0fa9ba0ef97650f83454d0fe5bf5c924717365d5cea3bdd73bfb06b5e0ede7d49c6da2caa8d38c5a337bae3c98c22b86d5f1c25740785c66ac195509
7
+ data.tar.gz: 719f1f95950ca24c0695316af978866e1b8834b5112c962701324d8279faa9e5705151157d23a66e7f9e6572313df18f7c4acfc247037965d059f12bdcacb883
data/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2025-02-19
9
+
10
+ - Allow specifying the version via DSL keyword: https://github.com/funwarioisii/mcp-rb/pull/2
11
+ - Add MCP Client: https://github.com/funwarioisii/mcp-rb/pull/3
12
+
13
+ ### Breaking Changes
14
+ - `MCP::PROTOCOL_VERSION` is moved to `MCP::Constants::PROTOCOL_VERSION`
15
+ - https://github.com/funwarioisii/mcp-rb/pull/3/commits/caad65500935a8eebfe024dbd25de0d16868c44e
16
+
17
+ ## [0.2.0] - 2025-02-14
18
+
19
+ ### Breaking Changes
20
+ - Unified DSL to block-based style for both tools and resources
21
+ - Example of new resource style:
22
+ ```ruby
23
+ resource "uri" do
24
+ name "Resource Name"
25
+ description "Description"
26
+ mime_type "text/plain"
27
+ call { "content" }
28
+ end
29
+ ```
30
+ - Example of new tool style:
31
+ ```ruby
32
+ tool "greet" do
33
+ description "Greet someone"
34
+ argument :name, String, required: true, description: "Name to greet"
35
+ call do |args|
36
+ "Hello, #{args[:name]}!"
37
+ end
38
+ end
39
+ ```
40
+
8
41
  ## [0.1.0] - 2025-02-12
9
42
 
10
43
  ### Added
data/README.md CHANGED
@@ -19,29 +19,40 @@ require 'mcp'
19
19
 
20
20
  name "hello-world"
21
21
 
22
- # リソースの定義
23
- resource "hello://world",
24
- name: "Hello World",
25
- description: "A simple hello world message" do
26
- "Hello, World!"
22
+ # Define a resource
23
+ resource "hello://world" do
24
+ name "Hello World"
25
+ description "A simple hello world message"
26
+ call { "Hello, World!" }
27
27
  end
28
28
 
29
- tool "greet",
30
- description: "Greet someone by name",
31
- input_schema: {
32
- type: :object,
33
- properties: {
34
- name: {
35
- type: :string,
36
- description: "Name to greet"
37
- }
38
- },
39
- required: [:name]
40
- } do |args|
41
- "Hello, #{args[:name]}!"
29
+ # Define a tool
30
+ tool "greet" do
31
+ description "Greet someone by name"
32
+ argument :name, String, required: true, description: "Name to greet"
33
+ call do |args|
34
+ "Hello, #{args[:name]}!"
35
+ end
42
36
  end
43
37
  ```
44
38
 
39
+ ## Supported specifications
40
+
41
+ Reference: [MCP 2024-11-05](https://spec.modelcontextprotocol.io/specification/2024-11-05/)
42
+
43
+ - [Base Protocol](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/)
44
+ - ping
45
+ - stdio transport
46
+ - [Server features](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/)
47
+ - Resources
48
+ - resources/read
49
+ - resources/list
50
+ - Tools
51
+ - tools/list
52
+ - tools/call
53
+
54
+ Any capabilities are not supported yet.
55
+
45
56
  ## Testing
46
57
 
47
58
  ```bash
@@ -65,3 +76,30 @@ Find broken using `hello_world.rb`
65
76
  ```bash
66
77
  bundle exec standardrb --fix
67
78
  ```
79
+
80
+ ## Release
81
+
82
+ To release a new version:
83
+
84
+ 1. Update version in `lib/mcp/version.rb`
85
+ 2. Update `CHANGELOG.md`
86
+ 3. Create a git tag
87
+
88
+ ```bash
89
+ git add .
90
+ git commit -m "Release vx.y.z"
91
+ git tag vx.y.z
92
+ git push --tags
93
+ ```
94
+
95
+ 1. Build and push to RubyGems
96
+
97
+ ```bash
98
+ gem build mcp-rb.gemspec
99
+ gem push mcp-rb-*.gem
100
+ ```
101
+
102
+ ## Changelog
103
+
104
+ See [CHANGELOG.md](CHANGELOG.md)
105
+
@@ -3,14 +3,64 @@
3
3
  module MCP
4
4
  class App
5
5
  module Resource
6
- def register_resource(uri, name:, mime_type: "text/plain", description: "", &block)
7
- raise ArgumentError, "Resource name cannot be nil or empty" if uri.nil? || uri.empty?
8
- raise ArgumentError, "Block must be provided" unless block_given?
6
+ def resources
7
+ @resources ||= {}
8
+ end
9
9
 
10
- resources[uri] = {
11
- uri:, name:, mime_type:, description:,
12
- handler: block
13
- }
10
+ class ResourceBuilder
11
+ attr_reader :uri, :name, :description, :mime_type, :handler
12
+
13
+ def initialize(uri)
14
+ raise ArgumentError, "Resource URI cannot be nil or empty" if uri.nil? || uri.empty?
15
+ @uri = uri
16
+ @name = ""
17
+ @description = ""
18
+ @mime_type = "text/plain"
19
+ @handler = nil
20
+ end
21
+
22
+ # standard:disable Lint/DuplicateMethods,Style/TrivialAccessors
23
+ def name(value)
24
+ @name = value
25
+ end
26
+ # standard:enable Lint/DuplicateMethods,Style/TrivialAccessors
27
+
28
+ # standard:disable Lint/DuplicateMethods,Style/TrivialAccessors
29
+ def description(text)
30
+ @description = text
31
+ end
32
+ # standard:enable Lint/DuplicateMethods,Style/TrivialAccessors
33
+
34
+ # standard:disable Lint/DuplicateMethods,Style/TrivialAccessors
35
+ def mime_type(value)
36
+ @mime_type = value
37
+ end
38
+ # standard:enable Lint/DuplicateMethods,Style/TrivialAccessors
39
+
40
+ def call(&block)
41
+ @handler = block
42
+ end
43
+
44
+ def to_resource_hash
45
+ raise ArgumentError, "Handler must be provided" unless @handler
46
+ raise ArgumentError, "Name must be provided" if @name.empty?
47
+
48
+ {
49
+ uri: @uri,
50
+ name: @name,
51
+ mime_type: @mime_type,
52
+ description: @description,
53
+ handler: @handler
54
+ }
55
+ end
56
+ end
57
+
58
+ def register_resource(uri, &block)
59
+ builder = ResourceBuilder.new(uri)
60
+ builder.instance_eval(&block)
61
+ resource_hash = builder.to_resource_hash
62
+ resources[uri] = resource_hash
63
+ resource_hash
14
64
  end
15
65
 
16
66
  def list_resources(cursor: nil, page_size: nil)
@@ -52,10 +102,6 @@ module MCP
52
102
 
53
103
  private
54
104
 
55
- def resources
56
- @resources ||= {}
57
- end
58
-
59
105
  def format_resource(resource)
60
106
  {
61
107
  uri: resource[:uri],
data/lib/mcp/app/tool.rb CHANGED
@@ -3,16 +3,74 @@
3
3
  module MCP
4
4
  class App
5
5
  module Tool
6
- def register_tool(name, description: "", input_schema: {}, &block)
7
- raise ArgumentError, "Tool name cannot be nil or empty" if name.nil? || name.empty?
8
- raise ArgumentError, "Block must be provided" unless block_given?
9
-
10
- tools[name] = {
11
- name: name,
12
- description: description,
13
- input_schema: input_schema,
14
- handler: block
15
- }
6
+ def tools
7
+ @tools ||= {}
8
+ end
9
+
10
+ class ToolBuilder
11
+ attr_reader :name, :description, :arguments, :handler
12
+
13
+ def initialize(name)
14
+ raise ArgumentError, "Tool name cannot be nil or empty" if name.nil? || name.empty?
15
+ @name = name
16
+ @description = ""
17
+ @arguments = {}
18
+ @required_arguments = []
19
+ @handler = nil
20
+ end
21
+
22
+ # standard:disable Lint/DuplicateMethods
23
+ def description(text = nil)
24
+ return @description if text.nil?
25
+ @description = text
26
+ end
27
+ # standard:enable Lint/DuplicateMethods
28
+
29
+ def argument(name, type, required: false, description: "")
30
+ @arguments[name] = {
31
+ type: ruby_type_to_schema_type(type),
32
+ description: description
33
+ }
34
+ @required_arguments << name if required
35
+ end
36
+
37
+ def call(&block)
38
+ @handler = block if block_given?
39
+ end
40
+
41
+ def to_tool_hash
42
+ raise ArgumentError, "Handler must be provided" unless @handler
43
+ {
44
+ name: @name,
45
+ description: @description,
46
+ input_schema: {
47
+ type: :object,
48
+ properties: @arguments,
49
+ required: @required_arguments
50
+ },
51
+ handler: @handler
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ def ruby_type_to_schema_type(type)
58
+ case type.to_s
59
+ when "String" then :string
60
+ when "Integer" then :integer
61
+ when "Float" then :number
62
+ when "TrueClass", "FalseClass", "Boolean" then :boolean
63
+ else :object
64
+ end
65
+ end
66
+ end
67
+
68
+ def register_tool(name, &block)
69
+ builder = ToolBuilder.new(name)
70
+ builder.instance_eval(&block)
71
+ tool_hash = builder.to_tool_hash
72
+ tools[name] = tool_hash
73
+ tool_hash
16
74
  end
17
75
 
18
76
  def list_tools(cursor: nil, page_size: 10)
@@ -32,6 +90,7 @@ module MCP
32
90
  raise ArgumentError, "Tool not found: #{name}" unless tool
33
91
 
34
92
  begin
93
+ validate_arguments(tool[:input_schema], arguments)
35
94
  result = tool[:handler].call(arguments)
36
95
  {
37
96
  content: [
@@ -57,8 +116,14 @@ module MCP
57
116
 
58
117
  private
59
118
 
60
- def tools
61
- @tools ||= {}
119
+ def validate_arguments(schema, arguments)
120
+ return unless schema[:required]
121
+
122
+ schema[:required].each do |required_arg|
123
+ unless arguments.key?(required_arg)
124
+ raise ArgumentError, "missing keyword: :#{required_arg}"
125
+ end
126
+ end
62
127
  end
63
128
 
64
129
  def format_tool(tool)
data/lib/mcp/client.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "securerandom"
6
+ require_relative "constants"
7
+
8
+ module MCP
9
+ class Client
10
+ attr_reader :command, :args, :process, :stdin, :stdout, :stderr, :wait_thread
11
+
12
+ def initialize(command:, args: [], name: "mcp-client", version: VERSION)
13
+ @command = command
14
+ @args = args
15
+ @process = nil
16
+ @name = name
17
+ @version = version
18
+ end
19
+
20
+ def connect
21
+ return if @process
22
+
23
+ start_server
24
+ initialize_connection
25
+ self
26
+ end
27
+
28
+ def running? = !@process.nil?
29
+
30
+ def list_tools
31
+ ensure_running
32
+ send_request({
33
+ jsonrpc: Constants::JSON_RPC_VERSION,
34
+ method: Constants::RequestMethods::TOOLS_LIST,
35
+ params: {},
36
+ id: SecureRandom.uuid
37
+ })
38
+ end
39
+
40
+ def call_tool(name:, args: {})
41
+ ensure_running
42
+ send_request({
43
+ jsonrpc: Constants::JSON_RPC_VERSION,
44
+ method: Constants::RequestMethods::TOOLS_CALL,
45
+ params: {
46
+ name: name,
47
+ arguments: args
48
+ },
49
+ id: SecureRandom.uuid
50
+ })
51
+ end
52
+
53
+ def close
54
+ return unless @process
55
+
56
+ @stdin.close
57
+ @stdout.close
58
+ @stderr.close
59
+ Process.kill("TERM", @process)
60
+ @wait_thread.join
61
+ @process = nil
62
+ rescue IOError, Errno::ESRCH
63
+ # プロセスが既に終了している場合は無視
64
+ @process = nil
65
+ end
66
+
67
+ private
68
+
69
+ def ensure_running
70
+ raise "Server process not running. Call #start first." unless running?
71
+ end
72
+
73
+ def initialize_connection
74
+ response = send_request({
75
+ jsonrpc: Constants::JSON_RPC_VERSION,
76
+ method: "initialize",
77
+ params: {
78
+ protocolVersion: Constants::PROTOCOL_VERSION,
79
+ client: {
80
+ name: @name,
81
+ version: @version
82
+ }
83
+ },
84
+ id: SecureRandom.uuid
85
+ })
86
+
87
+ @stdin.puts(JSON.generate({
88
+ jsonrpc: Constants::JSON_RPC_VERSION,
89
+ method: "notifications/initialized"
90
+ }))
91
+
92
+ response
93
+ end
94
+
95
+ def start_server
96
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@command, *@args)
97
+ @process = @wait_thread.pid
98
+
99
+ Thread.new do
100
+ while (line = @stderr.gets)
101
+ warn "[MCP Server] #{line}"
102
+ end
103
+ rescue IOError
104
+ # ignore when stream is closed
105
+ end
106
+ end
107
+
108
+ def send_request(request)
109
+ @stdin.puts(JSON.generate(request))
110
+ response = @stdout.gets
111
+ raise "No response from server" unless response
112
+
113
+ result = JSON.parse(response, symbolize_names: true)
114
+ if result[:error]
115
+ raise "Server error: #{result[:error][:message]} (#{result[:error][:code]})"
116
+ end
117
+
118
+ result[:result]
119
+ rescue JSON::ParserError => e
120
+ raise "Invalid JSON response: #{e.message}"
121
+ end
122
+ end
123
+ end
data/lib/mcp/constants.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module MCP
4
4
  module Constants
5
5
  JSON_RPC_VERSION = "2.0"
6
+ PROTOCOL_VERSION = "2024-11-05"
6
7
 
7
8
  module ErrorCodes
8
9
  NOT_INITIALIZED = -32_002
data/lib/mcp/delegator.rb CHANGED
@@ -15,6 +15,6 @@ module MCP
15
15
  end
16
16
  end
17
17
 
18
- delegate :name, :resource, :tool
18
+ delegate :name, :version, :resource, :tool
19
19
  end
20
20
  end
data/lib/mcp/server.rb CHANGED
@@ -3,32 +3,39 @@
3
3
  require "json"
4
4
  require "English"
5
5
  require "uri"
6
+ require_relative "constants"
6
7
 
7
8
  module MCP
8
9
  class Server
9
- attr_accessor :name
10
+ attr_writer :name, :version
10
11
  attr_reader :initialized
11
12
 
12
- def initialize(name:, version: VERSION)
13
+ def initialize(name:, version: "0.1.0")
13
14
  @name = name
14
15
  @version = version
15
16
  @app = App.new
16
17
  @initialized = false
17
- @supported_protocol_versions = [PROTOCOL_VERSION]
18
+ @supported_protocol_versions = [Constants::PROTOCOL_VERSION]
18
19
  end
19
20
 
20
- def name(value = nil) # standard:disable Lint/DuplicateMethods
21
+ def name(value = nil)
21
22
  return @name if value.nil?
22
23
 
23
24
  @name = value
24
25
  end
25
26
 
26
- def tool(name, description: "", input_schema: {}, &block)
27
- @app.register_tool(name, description: description, input_schema: input_schema, &block)
27
+ def version(value = nil)
28
+ return @version if value.nil?
29
+
30
+ @version = value
31
+ end
32
+
33
+ def tool(name, &block)
34
+ @app.register_tool(name, &block)
28
35
  end
29
36
 
30
- def resource(uri, name:, mime_type: "text/plain", description: "", &block)
31
- @app.register_resource(uri, name: name, mime_type: mime_type, description: description, &block)
37
+ def resource(uri, &block)
38
+ @app.register_resource(uri, &block)
32
39
  end
33
40
 
34
41
  def run
@@ -113,7 +120,7 @@ module MCP
113
120
  jsonrpc: MCP::Constants::JSON_RPC_VERSION,
114
121
  id: request[:id],
115
122
  result: {
116
- protocolVersion: PROTOCOL_VERSION,
123
+ protocolVersion: Constants::PROTOCOL_VERSION,
117
124
  capabilities: {
118
125
  logging: {},
119
126
  prompts: {
data/lib/mcp/version.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- PROTOCOL_VERSION = "2024-11-05"
5
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
6
5
  end
data/lib/mcp.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "mcp/constants"
8
8
  require_relative "mcp/app"
9
9
  require_relative "mcp/server"
10
10
  require_relative "mcp/delegator"
11
+ require_relative "mcp/client"
11
12
 
12
13
  module MCP
13
14
  class << self
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - funwarioisii
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-12 00:00:00.000000000 Z
10
+ date: 2025-02-19 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: MCP-RB is a Ruby framework that provides a Sinatra-like DSL for implementing
13
13
  Model Context Protocol servers.
@@ -24,6 +24,7 @@ files:
24
24
  - lib/mcp/app.rb
25
25
  - lib/mcp/app/resource.rb
26
26
  - lib/mcp/app/tool.rb
27
+ - lib/mcp/client.rb
27
28
  - lib/mcp/constants.rb
28
29
  - lib/mcp/delegator.rb
29
30
  - lib/mcp/server.rb
@@ -49,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
50
  - !ruby/object:Gem::Version
50
51
  version: '0'
51
52
  requirements: []
52
- rubygems_version: 3.6.2
53
+ rubygems_version: 3.6.3
53
54
  specification_version: 4
54
55
  summary: A lightweight Ruby framework for implementing MCP (Model Context Protocol)
55
56
  servers