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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +22 -7
- data/.github/workflows/release.yml +34 -2
- data/.rubocop.yml +3 -0
- data/AGENTS.md +107 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +6 -4
- data/README.md +135 -39
- data/RELEASE.md +12 -0
- data/bin/generate-gh-pages.sh +119 -0
- data/dev.yml +1 -2
- data/docs/_config.yml +6 -0
- data/docs/index.md +7 -0
- data/docs/latest/index.html +19 -0
- data/examples/http_server.rb +0 -2
- data/examples/stdio_server.rb +0 -1
- data/examples/streamable_http_server.rb +0 -2
- data/lib/json_rpc_handler.rb +151 -0
- data/lib/mcp/client/http.rb +23 -7
- data/lib/mcp/client.rb +62 -5
- data/lib/mcp/configuration.rb +38 -14
- data/lib/mcp/content.rb +2 -3
- data/lib/mcp/icon.rb +22 -0
- data/lib/mcp/instrumentation.rb +1 -1
- data/lib/mcp/methods.rb +3 -0
- data/lib/mcp/prompt/argument.rb +9 -5
- data/lib/mcp/prompt/message.rb +1 -2
- data/lib/mcp/prompt/result.rb +1 -2
- data/lib/mcp/prompt.rb +32 -4
- data/lib/mcp/resource/contents.rb +1 -2
- data/lib/mcp/resource/embedded.rb +1 -2
- data/lib/mcp/resource.rb +4 -3
- data/lib/mcp/resource_template.rb +4 -3
- data/lib/mcp/server/transports/streamable_http_transport.rb +96 -18
- data/lib/mcp/server.rb +92 -26
- data/lib/mcp/string_utils.rb +3 -4
- data/lib/mcp/tool/annotations.rb +1 -1
- data/lib/mcp/tool/input_schema.rb +6 -52
- data/lib/mcp/tool/output_schema.rb +3 -51
- data/lib/mcp/tool/response.rb +5 -4
- data/lib/mcp/tool/schema.rb +65 -0
- data/lib/mcp/tool.rb +47 -8
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- data/mcp.gemspec +5 -2
- metadata +16 -18
- 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
data/docs/_config.yml
ADDED
data/docs/index.md
ADDED
|
@@ -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>
|
data/examples/http_server.rb
CHANGED
data/examples/stdio_server.rb
CHANGED
|
@@ -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
|
data/lib/mcp/client/http.rb
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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 [
|
|
79
|
+
# @return [Hash] The full JSON-RPC response from the transport.
|
|
52
80
|
#
|
|
53
81
|
# @example
|
|
54
82
|
# tool = client.tools.first
|
|
55
|
-
#
|
|
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
|
-
|
|
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.
|
|
125
|
+
response.fetch("result", {})
|
|
69
126
|
end
|
|
70
127
|
|
|
71
128
|
private
|
data/lib/mcp/configuration.rb
CHANGED
|
@@ -2,29 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Configuration
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
16
|
-
|
|
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 ||
|
|
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
|
|
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
|
|
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
|