mcp 0.3.0 → 0.4.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: 984645dda3d0d04a93b2831354691c475b42d2826462156167a3f1e1104b85d0
4
- data.tar.gz: 20f96310129418791e0f5ae7f11b8a67605ff82e7ad29dfdfc414fd24157cf97
3
+ metadata.gz: 451bdb587fa89621924916d7cc137cf44a3a2293cf7803c3c6af1d97fb57332a
4
+ data.tar.gz: 3175f6a5d60426d5441b99b2bbf6d50b90d31daa9b713796029547f5efc6ab6a
5
5
  SHA512:
6
- metadata.gz: e9c323d818625f25999d1a2bb7ed1f16783cc295f8676c47bb58a96c284da133a2a5cc5c17086bc72216783325317543031288f1f8f804165503286ec0b1d2e8
7
- data.tar.gz: f0b2c995f44682257a642316c6df3aa20bde04db6de92842e35b323460238860890d3e197cc916ec68f96a57f8c24f12877805fc14b8fd27e46999bf4b3d2aa4
6
+ metadata.gz: 4b9e312c047ebc31d0826ec2520e14fef34fd0a23f42d9bf849f4a64b8cdb924341aa8a431aab497824849da476de5b8b2d55ad3b3996b9871e04b9022161aa3
7
+ data.tar.gz: 772762d9cf650278dd0756867a995e57e13366e1a4a2a5b306433d8d9246e8bed2aadb00e057d4f31a521603dab6f8fadcbcdfc9650806c79d9881e2e1913acb
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'github-actions'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'weekly'
@@ -13,7 +13,7 @@ jobs:
13
13
  - { ruby: head, allowed-failure: true }
14
14
  name: Test Ruby ${{ matrix.entry.ruby }}
15
15
  steps:
16
- - uses: actions/checkout@v3
16
+ - uses: actions/checkout@v5
17
17
  - uses: ruby/setup-ruby@v1
18
18
  with:
19
19
  ruby-version: ${{ matrix.entry.ruby }}
@@ -25,7 +25,7 @@ jobs:
25
25
  runs-on: ubuntu-latest
26
26
  name: RuboCop
27
27
  steps:
28
- - uses: actions/checkout@v3
28
+ - uses: actions/checkout@v5
29
29
  - uses: ruby/setup-ruby@v1
30
30
  with:
31
31
  ruby-version: 3.2 # Specify the oldest supported Ruby version.
@@ -16,7 +16,7 @@ jobs:
16
16
  id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
17
17
  contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v5
20
20
  - name: Set up Ruby
21
21
  uses: ruby/setup-ruby@v1
22
22
  with:
data/AGENTS.md ADDED
@@ -0,0 +1,119 @@
1
+ # AGENTS.md
2
+
3
+ ## Project overview
4
+
5
+ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing both server and client functionality for JSON-RPC 2.0 based communication between LLM applications and context providers.
6
+
7
+ ## Dev environment setup
8
+
9
+ - Ruby 3.2.0+ required
10
+ - Run `bundle install` to install dependencies
11
+ - Dependencies: `json_rpc_handler` ~> 0.1, `json-schema` >= 4.1
12
+
13
+ ## Build and test commands
14
+
15
+ - `bundle install` - Install dependencies
16
+ - `rake test` - Run all tests
17
+ - `rake rubocop` - Run linter
18
+ - `rake` - Run tests and linting (default task)
19
+ - `ruby -I lib -I test test/path/to/specific_test.rb` - Run single test file
20
+ - `gem build mcp.gemspec` - Build the gem
21
+
22
+ ## Testing instructions
23
+
24
+ - Test files are in `test/` directory with `_test.rb` suffix
25
+ - Run full test suite with `rake test`
26
+ - Run individual tests with `ruby -I lib -I test test/path/to/file_test.rb`
27
+ - Tests should pass before submitting PRs
28
+
29
+ ## Code style guidelines
30
+
31
+ - Follow RuboCop rules (run `rake rubocop`)
32
+ - Use frozen string literals
33
+ - Follow Ruby community conventions
34
+ - Keep dependencies minimal
35
+
36
+ ## Commit message conventions
37
+
38
+ - Use conventional commit format when possible
39
+ - Include clear, descriptive commit messages
40
+ - Releases are triggered by updating version in `lib/mcp/version.rb` and merging to main
41
+
42
+ ## Release process
43
+
44
+ - Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format in CHANGELOG.md
45
+ - Update CHANGELOG.md before cutting releases
46
+ - Use git history and PR merge commits to construct changelog entries
47
+ - Format entries as: "Terse description of the change (#nnn)"
48
+ - Keep entries in flat list format (no nesting)
49
+ - Git tags mark commits that cut new releases
50
+ - Exclude maintenance PRs that don't concern end users
51
+ - Check upstream remote for PRs if available
52
+
53
+ ## Architecture overview
54
+
55
+ ### Core Components
56
+
57
+ **MCP::Server** (`lib/mcp/server.rb`):
58
+
59
+ - Main server class handling JSON-RPC requests
60
+ - Implements MCP protocol methods: initialize, ping, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/read
61
+ - Supports custom method registration via `define_custom_method`
62
+ - Handles instrumentation, exception reporting, and notifications
63
+ - Uses JsonRpcHandler for request processing
64
+
65
+ **MCP::Client** (`lib/mcp/client.rb`):
66
+
67
+ - Client interface for communicating with MCP servers
68
+ - Transport-agnostic design with pluggable transport layers
69
+ - Supports tool listing and invocation
70
+
71
+ **Transport Layer**:
72
+
73
+ - `MCP::Server::Transports::StdioTransport` - Command-line stdio transport
74
+ - `MCP::Server::Transports::StreamableHttpTransport` - HTTP with streaming support
75
+ - `MCP::Client::HTTP` - HTTP client transport (requires faraday gem)
76
+
77
+ **Protocol Components**:
78
+
79
+ - `MCP::Tool` - Tool definition with input/output schemas and annotations
80
+ - `MCP::Prompt` - Prompt templates with argument validation
81
+ - `MCP::Resource` - Resource registration and retrieval
82
+ - `MCP::Configuration` - Global configuration with exception reporting and instrumentation
83
+
84
+ ### Key Patterns
85
+
86
+ **Three Ways to Define Components**:
87
+
88
+ 1. Class inheritance (e.g., `class MyTool < MCP::Tool`)
89
+ 2. Define methods (e.g., `MCP::Tool.define(name: "my_tool") { ... }`)
90
+ 3. Server registration (e.g., `server.define_tool(name: "my_tool") { ... }`)
91
+
92
+ **Schema Validation**:
93
+
94
+ - Tools support input_schema and output_schema for JSON Schema validation
95
+ - Protocol version 2025-03-26+ supports tool annotations (destructive_hint, idempotent_hint, etc.)
96
+ - Validation is configurable via `configuration.validate_tool_call_arguments`
97
+
98
+ **Context Passing**:
99
+
100
+ - `server_context` hash passed through tool/prompt calls for request-specific data
101
+ - Methods can accept `server_context:` keyword argument for accessing context
102
+
103
+ ### Dependencies
104
+
105
+ - `json_rpc_handler` ~> 0.1 - JSON-RPC 2.0 message handling
106
+ - `json-schema` >= 4.1 - Schema validation
107
+ - Ruby 3.2.0+ required
108
+
109
+ ### Integration patterns
110
+
111
+ - **Rails controllers**: Use `server.handle_json(request.body.read)` for HTTP endpoints
112
+ - **Command-line tools**: Use `StdioTransport.new(server).open` for CLI applications
113
+ - **HTTP services**: Use `StreamableHttpTransport` for web-based servers
114
+
115
+ ### Component definition patterns
116
+
117
+ 1. **Class inheritance**: `class MyTool < MCP::Tool`
118
+ 2. **Define methods**: `MCP::Tool.define(name: "my_tool") { ... }`
119
+ 3. **Server registration**: `server.define_tool(name: "my_tool") { ... }`
data/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2025-10-15
11
+
12
+ ### Added
13
+
14
+ - Client resources support with `resources/list` and `resources/read` methods (#160)
15
+ - `_meta` field support for Tool schema (#124)
16
+ - `_meta` field support for Prompt
17
+ - `title` field support for prompt arguments
18
+ - `call_tool_raw` method to client for accessing full tool responses (#149)
19
+ - Structured content support in tool responses (#147)
20
+ - AGENTS.md development guidance documentation (#134)
21
+ - Dependabot configuration for automated dependency updates (#138)
22
+
23
+ ### Changed
24
+
25
+ - Set default `content` to empty array instead of `nil` (#150)
26
+ - Improved prompt spec compliance (#153)
27
+ - Allow output schema to be array of objects (#144)
28
+ - Return 202 response code for accepted JSON-RPC notifications (#114)
29
+ - Added validation to `MCP::Configuration` setters (#145)
30
+ - Updated metaschema URI format for cross-OS compatibility
31
+
32
+ ### Fixed
33
+
34
+ - Client tools functionality and test coverage (#166)
35
+ - Client resources test for empty responses (#162)
36
+ - Documentation typos and incorrect examples (#157, #146)
37
+ - Removed redundant transport requires (#154)
38
+ - Cleaned up unused block parameters and magic comments
39
+
10
40
  ## [0.3.0] - 2025-09-14
11
41
 
12
42
  ### Added
data/README.md CHANGED
@@ -131,7 +131,7 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
131
131
 
132
132
  ```ruby
133
133
  server = MCP::Server.new(name: "my_server")
134
- transport = MCP::Transports::HTTP.new(server)
134
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
135
135
  server.transport = transport
136
136
 
137
137
  # When tools change, notify clients
@@ -178,7 +178,6 @@ If you want to build a local command-line application, you can use the stdio tra
178
178
 
179
179
  ```ruby
180
180
  require "mcp"
181
- require "mcp/server/transports/stdio_transport"
182
181
 
183
182
  # Create a simple tool
184
183
  class ExampleTool < MCP::Tool
@@ -425,10 +424,10 @@ tool = MCP::Tool.define(
425
424
  end
426
425
  ```
427
426
 
428
- 3. By using the `ModelContextProtocol::Server#define_tool` method with a block:
427
+ 3. By using the `MCP::Server#define_tool` method with a block:
429
428
 
430
429
  ```ruby
431
- server = ModelContextProtocol::Server.new
430
+ server = MCP::Server.new
432
431
  server.define_tool(
433
432
  name: "my_tool",
434
433
  description: "This tool performs specific functionality...",
@@ -546,6 +545,27 @@ class DataTool < MCP::Tool
546
545
  end
547
546
  ```
548
547
 
548
+ Output schema may also describe an array of objects:
549
+
550
+ ```ruby
551
+ class WeatherTool < MCP::Tool
552
+ output_schema(
553
+ type: "array",
554
+ item: {
555
+ properties: {
556
+ temperature: { type: "number" },
557
+ condition: { type: "string" },
558
+ humidity: { type: "integer" }
559
+ },
560
+ required: ["temperature", "condition", "humidity"]
561
+ }
562
+ )
563
+ end
564
+ ```
565
+
566
+ Please note: in this case, you must provide `type: "array"`. The default type
567
+ for output schemas is `object`.
568
+
549
569
  MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) specifies that:
550
570
 
551
571
  - **Server Validation**: Servers MUST provide structured results that conform to the output schema
@@ -555,6 +575,38 @@ MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2
555
575
 
556
576
  The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
557
577
 
578
+ ### Tool Responses with Structured Content
579
+
580
+ Tools can return structured data alongside text content using the `structured_content` parameter.
581
+
582
+ The structured content will be included in the JSON-RPC response as the `structuredContent` field.
583
+
584
+ ```ruby
585
+ class APITool < MCP::Tool
586
+ description "Get current weather and return structured data"
587
+
588
+ def self.call(endpoint:, server_context:)
589
+ # Call weather API and structure the response
590
+ api_response = WeatherAPI.fetch(location, units)
591
+ weather_data = {
592
+ temperature: api_response.temp,
593
+ condition: api_response.description,
594
+ humidity: api_response.humidity_percent
595
+ }
596
+
597
+ output_schema.validate_result(weather_data)
598
+
599
+ MCP::Tool::Response.new(
600
+ [{
601
+ type: "text",
602
+ text: weather_data.to_json
603
+ }],
604
+ structured_content: weather_data
605
+ )
606
+ end
607
+ end
608
+ ```
609
+
558
610
  ### Prompts
559
611
 
560
612
  MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
@@ -571,10 +623,12 @@ class MyPrompt < MCP::Prompt
571
623
  arguments [
572
624
  MCP::Prompt::Argument.new(
573
625
  name: "message",
626
+ title: "Message Title",
574
627
  description: "Input message",
575
628
  required: true
576
629
  )
577
630
  ]
631
+ meta({ version: "1.0", category: "example" })
578
632
 
579
633
  class << self
580
634
  def template(args, server_context:)
@@ -608,10 +662,12 @@ prompt = MCP::Prompt.define(
608
662
  arguments: [
609
663
  MCP::Prompt::Argument.new(
610
664
  name: "message",
665
+ title: "Message Title",
611
666
  description: "Input message",
612
667
  required: true
613
668
  )
614
- ]
669
+ ],
670
+ meta: { version: "1.0", category: "example" }
615
671
  ) do |args, server_context:|
616
672
  MCP::Prompt::Result.new(
617
673
  description: "Response description",
@@ -629,20 +685,22 @@ prompt = MCP::Prompt.define(
629
685
  end
630
686
  ```
631
687
 
632
- 3. Using the `ModelContextProtocol::Server#define_protocol` method:
688
+ 3. Using the `MCP::Server#define_prompt` method:
633
689
 
634
690
  ```ruby
635
- server = ModelContextProtocol::Server.new
636
- server.define_protocol(
691
+ server = MCP::Server.new
692
+ server.define_prompt(
637
693
  name: "my_prompt",
638
694
  description: "This prompt performs specific functionality...",
639
695
  arguments: [
640
696
  Prompt::Argument.new(
641
697
  name: "message",
698
+ title: "Message Title",
642
699
  description: "Input message",
643
700
  required: true
644
701
  )
645
- ]
702
+ ],
703
+ meta: { version: "1.0", category: "example" }
646
704
  ) do |args, server_context:|
647
705
  Prompt::Result.new(
648
706
  description: "Response description",
@@ -665,7 +723,7 @@ e.g. around authentication state or user preferences.
665
723
 
666
724
  ### Key Components
667
725
 
668
- - `MCP::Prompt::Argument` - Defines input parameters for the prompt template
726
+ - `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
669
727
  - `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
670
728
  - `MCP::Prompt::Result` - The output of a prompt template containing description and messages
671
729
  - `MCP::Content::Text` - Text content for messages
@@ -774,8 +832,10 @@ The `MCP::Client` class provides an interface for interacting with MCP servers.
774
832
 
775
833
  This class supports:
776
834
 
777
- - Tool listing via the `tools/list` method
778
- - Tool invocation via the `tools/call` method
835
+ - Tool listing via the `tools/list` method (`MCP::Client#tools`)
836
+ - Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
837
+ - Resource listing via the `resources/list` method (`MCP::Client#resources`)
838
+ - Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
779
839
  - Automatic JSON-RPC 2.0 message formatting
780
840
  - UUID request ID generation
781
841
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
4
  require "mcp"
5
- require "mcp/server/transports/streamable_http_transport"
6
5
  require "rack"
7
6
  require "rackup"
8
7
  require "json"
@@ -2,7 +2,6 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
4
  require "mcp"
5
- require "mcp/server/transports/stdio_transport"
6
5
 
7
6
  # Create a simple tool
8
7
  class ExampleTool < MCP::Tool
@@ -2,7 +2,6 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
4
  require "mcp"
5
- require "mcp/server/transports/streamable_http_transport"
6
5
  require "rack"
7
6
  require "rackup"
8
7
  require "json"
data/lib/mcp/client.rb CHANGED
@@ -44,28 +44,56 @@ module MCP
44
44
  end || []
45
45
  end
46
46
 
47
- # Calls a tool via the transport layer.
47
+ # Returns the list of resources available from the server.
48
+ # Each call will make a new request – the result is not cached.
49
+ #
50
+ # @return [Array<Hash>] An array of available resources.
51
+ def resources
52
+ response = transport.send_request(request: {
53
+ jsonrpc: JsonRpcHandler::Version::V2_0,
54
+ id: request_id,
55
+ method: "resources/list",
56
+ })
57
+
58
+ response.dig("result", "resources") || []
59
+ end
60
+
61
+ # Calls a tool via the transport layer and returns the full response from the server.
48
62
  #
49
63
  # @param tool [MCP::Client::Tool] The tool to be called.
50
64
  # @param arguments [Object, nil] The arguments to pass to the tool.
51
- # @return [Object] The result of the tool call, as returned by the transport.
65
+ # @return [Hash] The full JSON-RPC response from the transport.
52
66
  #
53
67
  # @example
54
68
  # tool = client.tools.first
55
- # result = client.call_tool(tool: tool, arguments: { foo: "bar" })
69
+ # response = client.call_tool(tool: tool, arguments: { foo: "bar" })
70
+ # structured_content = response.dig("result", "structuredContent")
56
71
  #
57
72
  # @note
58
73
  # The exact requirements for `arguments` are determined by the transport layer in use.
59
74
  # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
60
75
  def call_tool(tool:, arguments: nil)
61
- response = transport.send_request(request: {
76
+ transport.send_request(request: {
62
77
  jsonrpc: JsonRpcHandler::Version::V2_0,
63
78
  id: request_id,
64
79
  method: "tools/call",
65
80
  params: { name: tool.name, arguments: arguments },
66
81
  })
82
+ end
83
+
84
+ # Reads a resource from the server by URI and returns the contents.
85
+ #
86
+ # @param uri [String] The URI of the resource to read.
87
+ # @return [Array<Hash>] An array of resource contents (text or blob).
88
+ def read_resource(uri:)
89
+ response = transport.send_request(request: {
90
+ jsonrpc: JsonRpcHandler::Version::V2_0,
91
+ id: request_id,
92
+ method: "resources/read",
93
+ params: { uri: uri },
94
+ })
67
95
 
68
- response.dig("result", "content")
96
+ response.dig("result", "contents") || []
69
97
  end
70
98
 
71
99
  private
@@ -5,20 +5,29 @@ module MCP
5
5
  DEFAULT_PROTOCOL_VERSION = "2025-06-18"
6
6
  SUPPORTED_PROTOCOL_VERSIONS = [DEFAULT_PROTOCOL_VERSION, "2025-03-26", "2024-11-05"]
7
7
 
8
- attr_writer :exception_reporter, :instrumentation_callback, :protocol_version, :validate_tool_call_arguments
8
+ attr_writer :exception_reporter, :instrumentation_callback
9
9
 
10
10
  def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil,
11
11
  validate_tool_call_arguments: true)
12
12
  @exception_reporter = exception_reporter
13
13
  @instrumentation_callback = instrumentation_callback
14
14
  @protocol_version = protocol_version
15
- if protocol_version && !SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
16
- message = "protocol_version must be #{SUPPORTED_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_PROTOCOL_VERSIONS[-1]}"
17
- raise ArgumentError, message
18
- end
19
- unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
20
- raise ArgumentError, "validate_tool_call_arguments must be a boolean"
15
+ if protocol_version
16
+ validate_protocol_version!(protocol_version)
21
17
  end
18
+ validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
19
+
20
+ @validate_tool_call_arguments = validate_tool_call_arguments
21
+ end
22
+
23
+ def protocol_version=(protocol_version)
24
+ validate_protocol_version!(protocol_version)
25
+
26
+ @protocol_version = protocol_version
27
+ end
28
+
29
+ def validate_tool_call_arguments=(validate_tool_call_arguments)
30
+ validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
22
31
 
23
32
  @validate_tool_call_arguments = validate_tool_call_arguments
24
33
  end
@@ -83,6 +92,19 @@ module MCP
83
92
 
84
93
  private
85
94
 
95
+ def validate_protocol_version!(protocol_version)
96
+ unless SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
97
+ message = "protocol_version must be #{SUPPORTED_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_PROTOCOL_VERSIONS[-1]}"
98
+ raise ArgumentError, message
99
+ end
100
+ end
101
+
102
+ def validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
103
+ unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
104
+ raise ArgumentError, "validate_tool_call_arguments must be a boolean"
105
+ end
106
+ end
107
+
86
108
  def default_exception_reporter
87
109
  @default_exception_reporter ||= ->(exception, server_context) {}
88
110
  end
data/lib/mcp/content.rb CHANGED
@@ -1,4 +1,3 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -1,20 +1,24 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
5
4
  class Prompt
6
5
  class Argument
7
- attr_reader :name, :description, :required, :arguments
6
+ attr_reader :name, :title, :description, :required
8
7
 
9
- def initialize(name:, description: nil, required: false)
8
+ def initialize(name:, title: nil, description: nil, required: false)
10
9
  @name = name
10
+ @title = title
11
11
  @description = description
12
12
  @required = required
13
- @arguments = arguments
14
13
  end
15
14
 
16
15
  def to_h
17
- { name:, description:, required: }.compact
16
+ {
17
+ name: name,
18
+ title: title,
19
+ description: description,
20
+ required: required,
21
+ }.compact
18
22
  end
19
23
  end
20
24
  end
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
data/lib/mcp/prompt.rb CHANGED
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -9,13 +8,20 @@ module MCP
9
8
  attr_reader :title_value
10
9
  attr_reader :description_value
11
10
  attr_reader :arguments_value
11
+ attr_reader :meta_value
12
12
 
13
13
  def template(args, server_context: nil)
14
14
  raise NotImplementedError, "Subclasses must implement template"
15
15
  end
16
16
 
17
17
  def to_h
18
- { name: name_value, title: title_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
18
+ {
19
+ name: name_value,
20
+ title: title_value,
21
+ description: description_value,
22
+ arguments: arguments_value&.map(&:to_h),
23
+ _meta: meta_value,
24
+ }.compact
19
25
  end
20
26
 
21
27
  def inherited(subclass)
@@ -24,6 +30,7 @@ module MCP
24
30
  subclass.instance_variable_set(:@title_value, nil)
25
31
  subclass.instance_variable_set(:@description_value, nil)
26
32
  subclass.instance_variable_set(:@arguments_value, nil)
33
+ subclass.instance_variable_set(:@meta_value, nil)
27
34
  end
28
35
 
29
36
  def prompt_name(value = NOT_SET)
@@ -62,7 +69,15 @@ module MCP
62
69
  end
63
70
  end
64
71
 
65
- def define(name: nil, title: nil, description: nil, arguments: [], &block)
72
+ def meta(value = NOT_SET)
73
+ if value == NOT_SET
74
+ @meta_value
75
+ else
76
+ @meta_value = value
77
+ end
78
+ end
79
+
80
+ def define(name: nil, title: nil, description: nil, arguments: [], meta: nil, &block)
66
81
  Class.new(self) do
67
82
  prompt_name name
68
83
  title title
@@ -71,6 +86,7 @@ module MCP
71
86
  define_singleton_method(:template) do |args, server_context: nil|
72
87
  instance_exec(args, server_context:, &block)
73
88
  end
89
+ meta meta
74
90
  end
75
91
  end
76
92
 
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
data/lib/mcp/resource.rb CHANGED
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -108,8 +108,8 @@ module MCP
108
108
 
109
109
  if body["method"] == "initialize"
110
110
  handle_initialization(body_string, body)
111
- elsif body["method"] == MCP::Methods::NOTIFICATIONS_INITIALIZED
112
- handle_notification_initialized
111
+ elsif notification?(body) || response?(body)
112
+ handle_accepted
113
113
  else
114
114
  handle_regular_request(body_string, session_id)
115
115
  end
@@ -168,6 +168,14 @@ module MCP
168
168
  [400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
169
169
  end
170
170
 
171
+ def notification?(body)
172
+ !body["id"] && !!body["method"]
173
+ end
174
+
175
+ def response?(body)
176
+ !!body["id"] && !body["method"]
177
+ end
178
+
171
179
  def handle_initialization(body_string, body)
172
180
  session_id = SecureRandom.uuid
173
181
 
@@ -187,7 +195,7 @@ module MCP
187
195
  [200, headers, [response]]
188
196
  end
189
197
 
190
- def handle_notification_initialized
198
+ def handle_accepted
191
199
  [202, {}, []]
192
200
  end
193
201
 
data/lib/mcp/server.rb CHANGED
@@ -96,8 +96,8 @@ module MCP
96
96
  end
97
97
  end
98
98
 
99
- def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block)
100
- tool = Tool.define(name:, title:, description:, input_schema:, annotations:, &block)
99
+ def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, meta: nil, &block)
100
+ tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &block)
101
101
  @tools[tool.name_value] = tool
102
102
 
103
103
  validate!
@@ -253,7 +253,7 @@ module MCP
253
253
  end
254
254
 
255
255
  def list_tools(request)
256
- @tools.map { |_, tool| tool.to_h }
256
+ @tools.values.map(&:to_h)
257
257
  end
258
258
 
259
259
  def call_tool(request)
@@ -293,7 +293,7 @@ module MCP
293
293
  end
294
294
 
295
295
  def list_prompts(request)
296
- @prompts.map { |_, prompt| prompt.to_h }
296
+ @prompts.values.map(&:to_h)
297
297
  end
298
298
 
299
299
  def get_prompt(request)
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -50,7 +50,10 @@ module MCP
50
50
  accept_uri: false,
51
51
  accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
52
52
  )
53
- metaschema = JSON::Validator.validator_for_name("draft4").metaschema
53
+ metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
54
+ # Converts metaschema to a file URI for cross-platform compatibility
55
+ metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
56
+ metaschema = metaschema_uri.to_s
54
57
  errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
55
58
  if errors.any?
56
59
  raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
@@ -7,23 +7,20 @@ module MCP
7
7
  class OutputSchema
8
8
  class ValidationError < StandardError; end
9
9
 
10
- attr_reader :properties, :required
10
+ attr_reader :schema
11
11
 
12
- def initialize(properties: {}, required: [])
13
- @properties = properties
14
- @required = required.map(&:to_sym)
12
+ def initialize(schema = {})
13
+ @schema = deep_transform_keys(JSON.parse(JSON.dump(schema)), &:to_sym)
14
+ @schema[:type] ||= "object"
15
15
  validate_schema!
16
16
  end
17
17
 
18
18
  def ==(other)
19
- other.is_a?(OutputSchema) && properties == other.properties && required == other.required
19
+ other.is_a?(OutputSchema) && schema == other.schema
20
20
  end
21
21
 
22
22
  def to_h
23
- { type: "object" }.tap do |hsh|
24
- hsh[:properties] = properties if properties.any?
25
- hsh[:required] = required if required.any?
26
- end
23
+ @schema
27
24
  end
28
25
 
29
26
  def validate_result(result)
@@ -35,32 +32,38 @@ module MCP
35
32
 
36
33
  private
37
34
 
35
+ def deep_transform_keys(schema, &block)
36
+ case schema
37
+ when Hash
38
+ schema.each_with_object({}) do |(key, value), result|
39
+ if key.casecmp?("$ref")
40
+ raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
41
+ end
42
+
43
+ result[yield(key)] = deep_transform_keys(value, &block)
44
+ end
45
+ when Array
46
+ schema.map { |e| deep_transform_keys(e, &block) }
47
+ else
48
+ schema
49
+ end
50
+ end
51
+
38
52
  def validate_schema!
39
- check_for_refs!
40
53
  schema = to_h
41
54
  schema_reader = JSON::Schema::Reader.new(
42
55
  accept_uri: false,
43
56
  accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
44
57
  )
45
- metaschema = JSON::Validator.validator_for_name("draft4").metaschema
58
+ metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
59
+ # Converts metaschema to a file URI for cross-platform compatibility
60
+ metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
61
+ metaschema = metaschema_uri.to_s
46
62
  errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
47
63
  if errors.any?
48
64
  raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
49
65
  end
50
66
  end
51
-
52
- def check_for_refs!(obj = properties)
53
- case obj
54
- when Hash
55
- if obj.key?("$ref") || obj.key?(:$ref)
56
- raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
57
- end
58
-
59
- obj.each_value { |value| check_for_refs!(value) }
60
- when Array
61
- obj.each { |item| check_for_refs!(item) }
62
- end
63
- end
64
67
  end
65
68
  end
66
69
  end
@@ -5,16 +5,17 @@ module MCP
5
5
  class Response
6
6
  NOT_GIVEN = Object.new.freeze
7
7
 
8
- attr_reader :content
8
+ attr_reader :content, :structured_content
9
9
 
10
- def initialize(content, deprecated_error = NOT_GIVEN, error: false)
10
+ def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil)
11
11
  if deprecated_error != NOT_GIVEN
12
12
  warn("Passing `error` with the 2nd argument of `Response.new` is deprecated. Use keyword argument like `Response.new(content, error: error)` instead.", uplevel: 1)
13
13
  error = deprecated_error
14
14
  end
15
15
 
16
- @content = content
16
+ @content = content || []
17
17
  @error = error
18
+ @structured_content = structured_content
18
19
  end
19
20
 
20
21
  def error?
@@ -22,7 +23,7 @@ module MCP
22
23
  end
23
24
 
24
25
  def to_h
25
- { content:, isError: error? }.compact
26
+ { content:, isError: error?, structuredContent: @structured_content }.compact
26
27
  end
27
28
  end
28
29
  end
data/lib/mcp/tool.rb CHANGED
@@ -8,6 +8,7 @@ module MCP
8
8
  attr_reader :title_value
9
9
  attr_reader :description_value
10
10
  attr_reader :annotations_value
11
+ attr_reader :meta_value
11
12
 
12
13
  def call(*args, server_context: nil)
13
14
  raise NotImplementedError, "Subclasses must implement call"
@@ -21,6 +22,7 @@ module MCP
21
22
  inputSchema: input_schema_value.to_h,
22
23
  outputSchema: @output_schema_value&.to_h,
23
24
  annotations: annotations_value&.to_h,
25
+ _meta: meta_value,
24
26
  }.compact
25
27
  end
26
28
 
@@ -32,6 +34,7 @@ module MCP
32
34
  subclass.instance_variable_set(:@input_schema_value, nil)
33
35
  subclass.instance_variable_set(:@output_schema_value, nil)
34
36
  subclass.instance_variable_set(:@annotations_value, nil)
37
+ subclass.instance_variable_set(:@meta_value, nil)
35
38
  end
36
39
 
37
40
  def tool_name(value = NOT_SET)
@@ -84,14 +87,20 @@ module MCP
84
87
  if value == NOT_SET
85
88
  output_schema_value
86
89
  elsif value.is_a?(Hash)
87
- properties = value[:properties] || value["properties"] || {}
88
- required = value[:required] || value["required"] || []
89
- @output_schema_value = OutputSchema.new(properties:, required:)
90
+ @output_schema_value = OutputSchema.new(value)
90
91
  elsif value.is_a?(OutputSchema)
91
92
  @output_schema_value = value
92
93
  end
93
94
  end
94
95
 
96
+ def meta(value = NOT_SET)
97
+ if value == NOT_SET
98
+ @meta_value
99
+ else
100
+ @meta_value = value
101
+ end
102
+ end
103
+
95
104
  def annotations(hash = NOT_SET)
96
105
  if hash == NOT_SET
97
106
  @annotations_value
@@ -100,12 +109,13 @@ module MCP
100
109
  end
101
110
  end
102
111
 
103
- def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, annotations: nil, &block)
112
+ def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, meta: nil, annotations: nil, &block)
104
113
  Class.new(self) do
105
114
  tool_name name
106
115
  title title
107
116
  description description
108
117
  input_schema input_schema
118
+ meta meta
109
119
  output_schema output_schema
110
120
  self.annotations(annotations) if annotations
111
121
  define_singleton_method(:call, &block) if block
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -44,12 +44,13 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
- - ".cursor/rules/release-changelogs.mdc"
48
47
  - ".gitattributes"
48
+ - ".github/dependabot.yml"
49
49
  - ".github/workflows/ci.yml"
50
50
  - ".github/workflows/release.yml"
51
51
  - ".gitignore"
52
52
  - ".rubocop.yml"
53
+ - AGENTS.md
53
54
  - CHANGELOG.md
54
55
  - CODE_OF_CONDUCT.md
55
56
  - Gemfile
@@ -1,17 +0,0 @@
1
- ---
2
- description: Updating CHANGELOG.md before cutting a new release of the gem
3
- globs: CHANGELOG.md
4
- alwaysApply: false
5
- ---
6
-
7
- - start by refreshing your knowledge on the Keep a Changelog convention by reading the format spec referenced at the top of CHANGELOG.md
8
- - stick to Keep a Changelog
9
- - entries should be terse and in a top-level flat list: do not nest
10
- - follow this format for entries:
11
- - Terse description of the change (#nnn)
12
- - git tags are used to mark the commit that cut a new release of the gem
13
- - the gem version is located in [version.rb](mdc:lib/mcp/version.rb)
14
- - use the git history, especially merge commits from PRs to construct the changelog
15
- - when necessary, look at the diff of files changed to determine the true nature of the change
16
- - maintenance PRs that don't concern end users of the gem should not be listed in the changelog
17
- - when checking PRs, see if there's an upstream remote, and if so, fetch PRs from upstream instead of origin