mcp 0.3.0 → 0.5.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/ci.yml +22 -7
  4. data/.github/workflows/release.yml +34 -2
  5. data/.rubocop.yml +3 -0
  6. data/AGENTS.md +107 -0
  7. data/CHANGELOG.md +58 -0
  8. data/Gemfile +6 -4
  9. data/README.md +135 -39
  10. data/RELEASE.md +12 -0
  11. data/bin/generate-gh-pages.sh +119 -0
  12. data/dev.yml +1 -2
  13. data/docs/_config.yml +6 -0
  14. data/docs/index.md +7 -0
  15. data/docs/latest/index.html +19 -0
  16. data/examples/http_server.rb +0 -2
  17. data/examples/stdio_server.rb +0 -1
  18. data/examples/streamable_http_server.rb +0 -2
  19. data/lib/json_rpc_handler.rb +151 -0
  20. data/lib/mcp/client/http.rb +23 -7
  21. data/lib/mcp/client.rb +62 -5
  22. data/lib/mcp/configuration.rb +38 -14
  23. data/lib/mcp/content.rb +2 -3
  24. data/lib/mcp/icon.rb +22 -0
  25. data/lib/mcp/instrumentation.rb +1 -1
  26. data/lib/mcp/methods.rb +3 -0
  27. data/lib/mcp/prompt/argument.rb +9 -5
  28. data/lib/mcp/prompt/message.rb +1 -2
  29. data/lib/mcp/prompt/result.rb +1 -2
  30. data/lib/mcp/prompt.rb +32 -4
  31. data/lib/mcp/resource/contents.rb +1 -2
  32. data/lib/mcp/resource/embedded.rb +1 -2
  33. data/lib/mcp/resource.rb +4 -3
  34. data/lib/mcp/resource_template.rb +4 -3
  35. data/lib/mcp/server/transports/streamable_http_transport.rb +96 -18
  36. data/lib/mcp/server.rb +92 -26
  37. data/lib/mcp/string_utils.rb +3 -4
  38. data/lib/mcp/tool/annotations.rb +1 -1
  39. data/lib/mcp/tool/input_schema.rb +6 -52
  40. data/lib/mcp/tool/output_schema.rb +3 -51
  41. data/lib/mcp/tool/response.rb +5 -4
  42. data/lib/mcp/tool/schema.rb +65 -0
  43. data/lib/mcp/tool.rb +47 -8
  44. data/lib/mcp/version.rb +1 -1
  45. data/lib/mcp.rb +2 -0
  46. data/mcp.gemspec +5 -2
  47. metadata +16 -18
  48. data/.cursor/rules/release-changelogs.mdc +0 -17
data/RELEASE.md ADDED
@@ -0,0 +1,12 @@
1
+ ## Releases
2
+
3
+ This gem is published to [RubyGems.org](https://rubygems.org/gems/mcp)
4
+
5
+ Releases are triggered by PRs to the `main` branch updating the version number in `lib/mcp/version.rb`.
6
+
7
+ 1. **Update the version number** in `lib/mcp/version.rb`, following [semver](https://semver.org/)
8
+ 2. **Update CHANGELOG.md**, backfilling the changes since the last release if necessary, and adding a new section for the new version, clearing out the Unreleased section
9
+ 3. **Create a PR and get approval from a maintainer**
10
+ 4. **Merge your PR to the main branch** - This will automatically trigger the release workflow via GitHub Actions
11
+
12
+ When changes are merged to the `main` branch, the GitHub Actions workflow (`.github/workflows/release.yml`) is triggered and the gem is published to RubyGems.
@@ -0,0 +1,119 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Generates versioned documentation links and commits to gh-pages branch
5
+ #
6
+ # PURPOSE:
7
+ # This script generates a landing page with links to API documentation on
8
+ # RubyDoc.info for a specific version tag. This script is invoked by the
9
+ # publish-gh-pages job in the GitHub Actions workflow
10
+ # (.github/workflows/release.yml) when a release is published.
11
+ #
12
+ # HOW IT WORKS:
13
+ # - Creates isolated git worktrees for the specified tag and gh-pages branch
14
+ # - Copies static Jekyll template files from docs/
15
+ # - Generates _data/versions.yml with list of versions
16
+ # - Commits changes to gh-pages (does not push automatically)
17
+ #
18
+ # WORKFLOW:
19
+ # 1. Run this script with a tag name: `generate-gh-pages.sh v1.2.3`
20
+ # 2. Script generates docs and commits to local gh-pages branch
21
+ # 3. Push gh-pages branch to deploy: `git push origin gh-pages`
22
+
23
+ # Parse semantic version from tag name (ignoring arbitrary prefixes)
24
+ if [[ "${1}" =~ ([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?)$ ]]; then
25
+ VERSION="v${BASH_REMATCH[1]}"
26
+ else
27
+ echo "Error: Must specify a tag name that contains a valid semantic version"
28
+ echo "Usage: ${0} <tag-name>"
29
+ echo "Examples:"
30
+ echo " ${0} 1.2.3"
31
+ echo " ${0} v2.0.0-rc.1"
32
+ exit 1
33
+ fi
34
+
35
+ TAG_NAME="${1}"
36
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
37
+
38
+ echo "Generating documentation for tag: ${TAG_NAME}"
39
+
40
+ # Create temporary directories for both worktrees
41
+ WORKTREE_DIR=$(mktemp -d)
42
+ GHPAGES_WORKTREE_DIR=$(mktemp -d)
43
+
44
+ # Set up trap to clean up both worktrees on exit
45
+ trap 'git worktree remove --force "${WORKTREE_DIR}" 2>/dev/null || true; \
46
+ git worktree remove --force "${GHPAGES_WORKTREE_DIR}" 2>/dev/null || true' EXIT
47
+
48
+ echo "Creating worktree for ${TAG_NAME}..."
49
+ git worktree add --quiet "${WORKTREE_DIR}" "${TAG_NAME}"
50
+
51
+ # Check if gh-pages branch exists
52
+ if git show-ref --verify --quiet refs/heads/gh-pages; then
53
+ echo "Creating worktree for existing gh-pages branch..."
54
+ git worktree add --quiet "${GHPAGES_WORKTREE_DIR}" gh-pages
55
+ elif git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1; then
56
+ echo "Creating worktree for gh-pages branch from remote..."
57
+ git worktree add --quiet "${GHPAGES_WORKTREE_DIR}" -b gh-pages origin/gh-pages
58
+ else
59
+ echo "Creating worktree for new orphan gh-pages branch..."
60
+ git worktree add --quiet --detach "${GHPAGES_WORKTREE_DIR}"
61
+ git -C "${GHPAGES_WORKTREE_DIR}" checkout --orphan gh-pages
62
+ git -C "${GHPAGES_WORKTREE_DIR}" rm -rf . > /dev/null 2>&1 || true
63
+ fi
64
+
65
+ # Change to gh-pages worktree
66
+ cd "${GHPAGES_WORKTREE_DIR}"
67
+
68
+ # Determine if this tag is the latest version
69
+ echo "Determining if ${VERSION} is the latest version..."
70
+
71
+ # Get all existing version tags from the repository (reverse sorted, newest first)
72
+ ALL_VERSIONS=$(
73
+ git -C "${REPO_ROOT}" tag --list | \
74
+ sed -nE 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?)$/v\1/p' | \
75
+ sort -Vr
76
+ )
77
+
78
+ # Get the latest version from all version tags
79
+ LATEST_VERSION=$(echo "${ALL_VERSIONS}" | head -n 1)
80
+
81
+ if [ "${VERSION}" = "${LATEST_VERSION}" ]; then
82
+ echo "${VERSION} is the latest version"
83
+ else
84
+ echo "${VERSION} is not the latest version (latest is ${LATEST_VERSION})"
85
+ fi
86
+
87
+ # Update custom documentation for latest version
88
+ if [ "${VERSION}" = "${LATEST_VERSION}" ]; then
89
+ echo "Updating custom documentation..."
90
+
91
+ # Clean up old custom docs from gh-pages root
92
+ echo "Cleaning gh-pages root..."
93
+ git ls-tree --name-only HEAD | xargs -r git rm -rf
94
+
95
+ # Copy custom docs from docs/ directory
96
+ echo "Copying custom docs from ${WORKTREE_DIR}/docs/..."
97
+ cp -r "${WORKTREE_DIR}/docs/." "${GHPAGES_WORKTREE_DIR}/"
98
+ fi
99
+
100
+ # Generate version data for Jekyll
101
+ echo "Generating _data/versions.yml..."
102
+ mkdir -p _data
103
+ echo "${ALL_VERSIONS}" | sed 's/^v/- /' > _data/versions.yml
104
+
105
+ # Stage all changes
106
+ git add .
107
+
108
+ # Commit if there are changes
109
+ if git diff --staged --quiet; then
110
+ echo "No changes to commit"
111
+ else
112
+ echo "Committing documentation for ${VERSION}..."
113
+ git commit -m "Add ${VERSION} docs"
114
+
115
+ echo "Documentation committed to gh-pages branch!"
116
+ echo "Push to remote to deploy to GitHub Pages"
117
+ fi
118
+
119
+ echo "Done!"
data/dev.yml CHANGED
@@ -27,5 +27,4 @@ commands:
27
27
  style:
28
28
  desc: Run rubocop
29
29
  aliases: [rubocop, lint]
30
- run: bin/rubocop
31
-
30
+ run: bin/rake rubocop
data/docs/_config.yml ADDED
@@ -0,0 +1,6 @@
1
+ # Use package name as site title
2
+ title: "MCP Ruby SDK"
3
+
4
+ # Include generated files and directories which may start with underscores
5
+ include:
6
+ - "_*"
data/docs/index.md ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ # Empty Jekyll front matter to enable Liquid templating (see {{ ... }} below)
3
+ ---
4
+
5
+ {% for version in site.data.versions -%}
6
+ - [v{{ version }}](https://rubydoc.info/gems/mcp/{{ version }})
7
+ {% endfor %}
@@ -0,0 +1,19 @@
1
+ ---
2
+ # Empty Jekyll front matter to enable Liquid templating (see {{ ... }} below)
3
+ ---
4
+
5
+ <!DOCTYPE html>
6
+ <html>
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <title>Redirecting to latest documentation...</title>
10
+ <meta http-equiv="refresh" content="0; url=https://rubydoc.info/gems/mcp">
11
+ <link rel="canonical" href="https://rubydoc.info/gems/mcp">
12
+ </head>
13
+ <body>
14
+ <p>Redirecting to <a href="https://rubydoc.info/gems/mcp">latest documentation</a>...</p>
15
+ <script>
16
+ window.location.href = "https://rubydoc.info/gems/mcp";
17
+ </script>
18
+ </body>
19
+ </html>
@@ -2,8 +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
- require "rack"
7
5
  require "rackup"
8
6
  require "json"
9
7
  require "logger"
@@ -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,8 +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
- require "rack"
7
5
  require "rackup"
8
6
  require "json"
9
7
  require "logger"
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module JsonRpcHandler
6
+ class Version
7
+ V1_0 = "1.0"
8
+ V2_0 = "2.0"
9
+ end
10
+
11
+ class ErrorCode
12
+ INVALID_REQUEST = -32600
13
+ METHOD_NOT_FOUND = -32601
14
+ INVALID_PARAMS = -32602
15
+ INTERNAL_ERROR = -32603
16
+ PARSE_ERROR = -32700
17
+ end
18
+
19
+ DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z/
20
+
21
+ extend self
22
+
23
+ def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
24
+ if request.is_a?(Array)
25
+ return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
26
+ code: ErrorCode::INVALID_REQUEST,
27
+ message: "Invalid Request",
28
+ data: "Request is an empty array",
29
+ }) if request.empty?
30
+
31
+ # Handle batch requests
32
+ responses = request.map { |req| process_request(req, id_validation_pattern: id_validation_pattern, &method_finder) }.compact
33
+
34
+ # A single item is hoisted out of the array
35
+ return responses.first if responses.one?
36
+
37
+ # An empty array yields nil
38
+ responses if responses.any?
39
+ elsif request.is_a?(Hash)
40
+ # Handle single request
41
+ process_request(request, id_validation_pattern: id_validation_pattern, &method_finder)
42
+ else
43
+ error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
44
+ code: ErrorCode::INVALID_REQUEST,
45
+ message: "Invalid Request",
46
+ data: "Request must be an array or a hash",
47
+ })
48
+ end
49
+ end
50
+
51
+ def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
52
+ begin
53
+ request = JSON.parse(request_json, symbolize_names: true)
54
+ response = handle(request, id_validation_pattern: id_validation_pattern, &method_finder)
55
+ rescue JSON::ParserError
56
+ response = error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
57
+ code: ErrorCode::PARSE_ERROR,
58
+ message: "Parse error",
59
+ data: "Invalid JSON",
60
+ })
61
+ end
62
+
63
+ response&.to_json
64
+ end
65
+
66
+ def process_request(request, id_validation_pattern:, &method_finder)
67
+ id = request[:id]
68
+
69
+ error = if !valid_version?(request[:jsonrpc])
70
+ "JSON-RPC version must be 2.0"
71
+ elsif !valid_id?(request[:id], id_validation_pattern)
72
+ "Request ID must match validation pattern, or be an integer or null"
73
+ elsif !valid_method_name?(request[:method])
74
+ 'Method name must be a string and not start with "rpc."'
75
+ end
76
+
77
+ return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
78
+ code: ErrorCode::INVALID_REQUEST,
79
+ message: "Invalid Request",
80
+ data: error,
81
+ }) if error
82
+
83
+ method_name = request[:method]
84
+ params = request[:params]
85
+
86
+ unless valid_params?(params)
87
+ return error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
88
+ code: ErrorCode::INVALID_PARAMS,
89
+ message: "Invalid params",
90
+ data: "Method parameters must be an array or an object or null",
91
+ })
92
+ end
93
+
94
+ begin
95
+ method = method_finder.call(method_name)
96
+
97
+ if method.nil?
98
+ return error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
99
+ code: ErrorCode::METHOD_NOT_FOUND,
100
+ message: "Method not found",
101
+ data: method_name,
102
+ })
103
+ end
104
+
105
+ result = method.call(params)
106
+
107
+ success_response(id: id, result: result)
108
+ rescue StandardError => e
109
+ error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
110
+ code: ErrorCode::INTERNAL_ERROR,
111
+ message: "Internal error",
112
+ data: e.message,
113
+ })
114
+ end
115
+ end
116
+
117
+ def valid_version?(version)
118
+ version == Version::V2_0
119
+ end
120
+
121
+ def valid_id?(id, pattern = nil)
122
+ return true if id.nil? || id.is_a?(Integer)
123
+ return false unless id.is_a?(String)
124
+
125
+ pattern ? id.match?(pattern) : true
126
+ end
127
+
128
+ def valid_method_name?(method)
129
+ method.is_a?(String) && !method.start_with?("rpc.")
130
+ end
131
+
132
+ def valid_params?(params)
133
+ params.nil? || params.is_a?(Array) || params.is_a?(Hash)
134
+ end
135
+
136
+ def success_response(id:, result:)
137
+ {
138
+ jsonrpc: Version::V2_0,
139
+ id: id,
140
+ result: result,
141
+ } unless id.nil?
142
+ end
143
+
144
+ def error_response(id:, id_validation_pattern:, error:)
145
+ {
146
+ jsonrpc: Version::V2_0,
147
+ id: valid_id?(id, id_validation_pattern) ? id : nil,
148
+ error: error.compact,
149
+ } unless id.nil?
150
+ end
151
+ end
@@ -3,6 +3,8 @@
3
3
  module MCP
4
4
  class Client
5
5
  class HTTP
6
+ ACCEPT_HEADER = "application/json, text/event-stream"
7
+
6
8
  attr_reader :url
7
9
 
8
10
  def initialize(url:, headers: {})
@@ -14,46 +16,48 @@ module MCP
14
16
  method = request[:method] || request["method"]
15
17
  params = request[:params] || request["params"]
16
18
 
17
- client.post("", request).body
19
+ response = client.post("", request)
20
+ validate_response_content_type!(response, method, params)
21
+ response.body
18
22
  rescue Faraday::BadRequestError => e
19
23
  raise RequestHandlerError.new(
20
24
  "The #{method} request is invalid",
21
- { method:, params: },
25
+ { method: method, params: params },
22
26
  error_type: :bad_request,
23
27
  original_error: e,
24
28
  )
25
29
  rescue Faraday::UnauthorizedError => e
26
30
  raise RequestHandlerError.new(
27
31
  "You are unauthorized to make #{method} requests",
28
- { method:, params: },
32
+ { method: method, params: params },
29
33
  error_type: :unauthorized,
30
34
  original_error: e,
31
35
  )
32
36
  rescue Faraday::ForbiddenError => e
33
37
  raise RequestHandlerError.new(
34
38
  "You are forbidden to make #{method} requests",
35
- { method:, params: },
39
+ { method: method, params: params },
36
40
  error_type: :forbidden,
37
41
  original_error: e,
38
42
  )
39
43
  rescue Faraday::ResourceNotFound => e
40
44
  raise RequestHandlerError.new(
41
45
  "The #{method} request is not found",
42
- { method:, params: },
46
+ { method: method, params: params },
43
47
  error_type: :not_found,
44
48
  original_error: e,
45
49
  )
46
50
  rescue Faraday::UnprocessableEntityError => e
47
51
  raise RequestHandlerError.new(
48
52
  "The #{method} request is unprocessable",
49
- { method:, params: },
53
+ { method: method, params: params },
50
54
  error_type: :unprocessable_entity,
51
55
  original_error: e,
52
56
  )
53
57
  rescue Faraday::Error => e # Catch-all
54
58
  raise RequestHandlerError.new(
55
59
  "Internal error handling #{method} request",
56
- { method:, params: },
60
+ { method: method, params: params },
57
61
  error_type: :internal_error,
58
62
  original_error: e,
59
63
  )
@@ -70,6 +74,7 @@ module MCP
70
74
  faraday.response(:json)
71
75
  faraday.response(:raise_error)
72
76
 
77
+ faraday.headers["Accept"] = ACCEPT_HEADER
73
78
  headers.each do |key, value|
74
79
  faraday.headers[key] = value
75
80
  end
@@ -83,6 +88,17 @@ module MCP
83
88
  "Add it to your Gemfile: gem 'faraday', '>= 2.0'" \
84
89
  "See https://rubygems.org/gems/faraday for more details."
85
90
  end
91
+
92
+ def validate_response_content_type!(response, method, params)
93
+ content_type = response.headers["Content-Type"]
94
+ return if content_type&.include?("application/json")
95
+
96
+ raise RequestHandlerError.new(
97
+ "Unsupported Content-Type: #{content_type.inspect}. This client only supports JSON responses.",
98
+ { method: method, params: params },
99
+ error_type: :unsupported_media_type,
100
+ )
101
+ end
86
102
  end
87
103
  end
88
104
  end
data/lib/mcp/client.rb CHANGED
@@ -44,28 +44,85 @@ 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
+ # Returns the list of prompts available from the server.
62
+ # Each call will make a new request – the result is not cached.
63
+ #
64
+ # @return [Array<Hash>] An array of available prompts.
65
+ def prompts
66
+ response = transport.send_request(request: {
67
+ jsonrpc: JsonRpcHandler::Version::V2_0,
68
+ id: request_id,
69
+ method: "prompts/list",
70
+ })
71
+
72
+ response.dig("result", "prompts") || []
73
+ end
74
+
75
+ # Calls a tool via the transport layer and returns the full response from the server.
48
76
  #
49
77
  # @param tool [MCP::Client::Tool] The tool to be called.
50
78
  # @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.
79
+ # @return [Hash] The full JSON-RPC response from the transport.
52
80
  #
53
81
  # @example
54
82
  # tool = client.tools.first
55
- # result = client.call_tool(tool: tool, arguments: { foo: "bar" })
83
+ # response = client.call_tool(tool: tool, arguments: { foo: "bar" })
84
+ # structured_content = response.dig("result", "structuredContent")
56
85
  #
57
86
  # @note
58
87
  # The exact requirements for `arguments` are determined by the transport layer in use.
59
88
  # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
60
89
  def call_tool(tool:, arguments: nil)
61
- response = transport.send_request(request: {
90
+ transport.send_request(request: {
62
91
  jsonrpc: JsonRpcHandler::Version::V2_0,
63
92
  id: request_id,
64
93
  method: "tools/call",
65
94
  params: { name: tool.name, arguments: arguments },
66
95
  })
96
+ end
97
+
98
+ # Reads a resource from the server by URI and returns the contents.
99
+ #
100
+ # @param uri [String] The URI of the resource to read.
101
+ # @return [Array<Hash>] An array of resource contents (text or blob).
102
+ def read_resource(uri:)
103
+ response = transport.send_request(request: {
104
+ jsonrpc: JsonRpcHandler::Version::V2_0,
105
+ id: request_id,
106
+ method: "resources/read",
107
+ params: { uri: uri },
108
+ })
109
+
110
+ response.dig("result", "contents") || []
111
+ end
112
+
113
+ # Gets a prompt from the server by name and returns its details.
114
+ #
115
+ # @param name [String] The name of the prompt to get.
116
+ # @return [Hash] A hash containing the prompt details.
117
+ def get_prompt(name:)
118
+ response = transport.send_request(request: {
119
+ jsonrpc: JsonRpcHandler::Version::V2_0,
120
+ id: request_id,
121
+ method: "prompts/get",
122
+ params: { name: name },
123
+ })
67
124
 
68
- response.dig("result", "content")
125
+ response.fetch("result", {})
69
126
  end
70
127
 
71
128
  private
@@ -2,29 +2,40 @@
2
2
 
3
3
  module MCP
4
4
  class Configuration
5
- DEFAULT_PROTOCOL_VERSION = "2025-06-18"
6
- SUPPORTED_PROTOCOL_VERSIONS = [DEFAULT_PROTOCOL_VERSION, "2025-03-26", "2024-11-05"]
5
+ LATEST_STABLE_PROTOCOL_VERSION = "2025-11-25"
6
+ SUPPORTED_STABLE_PROTOCOL_VERSIONS = [
7
+ LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
8
+ ]
7
9
 
8
- attr_writer :exception_reporter, :instrumentation_callback, :protocol_version, :validate_tool_call_arguments
10
+ attr_writer :exception_reporter, :instrumentation_callback
9
11
 
10
12
  def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil,
11
13
  validate_tool_call_arguments: true)
12
14
  @exception_reporter = exception_reporter
13
15
  @instrumentation_callback = instrumentation_callback
14
16
  @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"
17
+ if protocol_version
18
+ validate_protocol_version!(protocol_version)
21
19
  end
20
+ validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
21
+
22
+ @validate_tool_call_arguments = validate_tool_call_arguments
23
+ end
24
+
25
+ def protocol_version=(protocol_version)
26
+ validate_protocol_version!(protocol_version)
27
+
28
+ @protocol_version = protocol_version
29
+ end
30
+
31
+ def validate_tool_call_arguments=(validate_tool_call_arguments)
32
+ validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
22
33
 
23
34
  @validate_tool_call_arguments = validate_tool_call_arguments
24
35
  end
25
36
 
26
37
  def protocol_version
27
- @protocol_version || DEFAULT_PROTOCOL_VERSION
38
+ @protocol_version || LATEST_STABLE_PROTOCOL_VERSION
28
39
  end
29
40
 
30
41
  def protocol_version?
@@ -74,15 +85,28 @@ module MCP
74
85
  validate_tool_call_arguments = other.validate_tool_call_arguments
75
86
 
76
87
  Configuration.new(
77
- exception_reporter:,
78
- instrumentation_callback:,
79
- protocol_version:,
80
- validate_tool_call_arguments:,
88
+ exception_reporter: exception_reporter,
89
+ instrumentation_callback: instrumentation_callback,
90
+ protocol_version: protocol_version,
91
+ validate_tool_call_arguments: validate_tool_call_arguments,
81
92
  )
82
93
  end
83
94
 
84
95
  private
85
96
 
97
+ def validate_protocol_version!(protocol_version)
98
+ unless SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
99
+ message = "protocol_version must be #{SUPPORTED_STABLE_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_STABLE_PROTOCOL_VERSIONS[-1]}"
100
+ raise ArgumentError, message
101
+ end
102
+ end
103
+
104
+ def validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
105
+ unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
106
+ raise ArgumentError, "validate_tool_call_arguments must be a boolean"
107
+ end
108
+ end
109
+
86
110
  def default_exception_reporter
87
111
  @default_exception_reporter ||= ->(exception, server_context) {}
88
112
  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
@@ -12,7 +11,7 @@ module MCP
12
11
  end
13
12
 
14
13
  def to_h
15
- { text:, annotations:, type: "text" }.compact
14
+ { text: text, annotations: annotations, type: "text" }.compact
16
15
  end
17
16
  end
18
17
 
@@ -26,7 +25,7 @@ module MCP
26
25
  end
27
26
 
28
27
  def to_h
29
- { data:, mime_type:, annotations:, type: "image" }.compact
28
+ { data: data, mime_type: mime_type, annotations: annotations, type: "image" }.compact
30
29
  end
31
30
  end
32
31
  end
data/lib/mcp/icon.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Icon
5
+ SUPPORTED_THEMES = ["light", "dark"]
6
+
7
+ attr_reader :mime_type, :sizes, :src, :theme
8
+
9
+ def initialize(mime_type: nil, sizes: nil, src: nil, theme: nil)
10
+ raise ArgumentError, 'The value of theme must specify "light" or "dark".' if theme && !SUPPORTED_THEMES.include?(theme)
11
+
12
+ @mime_type = mime_type
13
+ @sizes = sizes
14
+ @src = src
15
+ @theme = theme
16
+ end
17
+
18
+ def to_h
19
+ { mimeType: mime_type, sizes: sizes, src: src, theme: theme }.compact
20
+ end
21
+ end
22
+ end
@@ -6,7 +6,7 @@ module MCP
6
6
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
7
  begin
8
8
  @instrumentation_data = {}
9
- add_instrumentation_data(method:)
9
+ add_instrumentation_data(method: method)
10
10
 
11
11
  result = yield block
12
12