mcp 0.8.0 → 0.9.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/README.md +174 -5
- data/lib/mcp/client/stdio.rb +222 -0
- data/lib/mcp/client.rb +12 -2
- data/lib/mcp/progress.rb +21 -0
- data/lib/mcp/prompt.rb +4 -0
- data/lib/mcp/resource.rb +3 -0
- data/lib/mcp/server/transports/stdio_transport.rb +1 -1
- data/lib/mcp/server/transports/streamable_http_transport.rb +7 -19
- data/lib/mcp/server/transports.rb +10 -0
- data/lib/mcp/server.rb +40 -4
- data/lib/mcp/server_context.rb +26 -0
- data/lib/mcp/tool.rb +5 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +10 -24
- metadata +7 -36
- data/.gitattributes +0 -4
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/ci.yml +0 -54
- data/.github/workflows/conformance.yml +0 -29
- data/.github/workflows/release.yml +0 -57
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -15
- data/AGENTS.md +0 -107
- data/CHANGELOG.md +0 -168
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -29
- data/RELEASE.md +0 -12
- data/Rakefile +0 -56
- data/SECURITY.md +0 -21
- data/bin/console +0 -15
- data/bin/generate-gh-pages.sh +0 -119
- data/bin/rake +0 -31
- data/bin/setup +0 -8
- data/conformance/README.md +0 -103
- data/conformance/expected_failures.yml +0 -9
- data/conformance/runner.rb +0 -101
- data/conformance/server.rb +0 -547
- data/dev.yml +0 -30
- data/docs/_config.yml +0 -6
- data/docs/index.md +0 -7
- data/docs/latest/index.html +0 -19
- data/examples/README.md +0 -197
- data/examples/http_client.rb +0 -184
- data/examples/http_server.rb +0 -169
- data/examples/stdio_server.rb +0 -94
- data/examples/streamable_http_client.rb +0 -207
- data/examples/streamable_http_server.rb +0 -172
- data/mcp.gemspec +0 -35
data/conformance/runner.rb
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Starts the conformance server and runs `npx @modelcontextprotocol/conformance` against it.
|
|
4
|
-
require "English"
|
|
5
|
-
require "net/http"
|
|
6
|
-
require_relative "server"
|
|
7
|
-
|
|
8
|
-
module Conformance
|
|
9
|
-
class Runner
|
|
10
|
-
# Timeout for waiting for the Puma server to start.
|
|
11
|
-
SERVER_START_TIMEOUT = 20
|
|
12
|
-
SERVER_POLL_INTERVAL = 0.5
|
|
13
|
-
SERVER_HEALTH_CHECK_RETRIES = (SERVER_START_TIMEOUT / SERVER_POLL_INTERVAL).to_i
|
|
14
|
-
|
|
15
|
-
def initialize(port: Server::DEFAULT_PORT, scenario: nil, spec_version: nil, verbose: false)
|
|
16
|
-
@port = port
|
|
17
|
-
@scenario = scenario
|
|
18
|
-
@spec_version = spec_version
|
|
19
|
-
@verbose = verbose
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def run
|
|
23
|
-
command = build_command
|
|
24
|
-
server_pid = start_server
|
|
25
|
-
|
|
26
|
-
run_conformance(command, server_pid: server_pid)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def build_command
|
|
32
|
-
expected_failures_yml = File.expand_path("expected_failures.yml", __dir__)
|
|
33
|
-
|
|
34
|
-
npx_command = ["npx", "--yes", "@modelcontextprotocol/conformance", "server", "--url", "http://localhost:#{@port}/mcp"]
|
|
35
|
-
npx_command += ["--scenario", @scenario] if @scenario
|
|
36
|
-
npx_command += ["--spec-version", @spec_version] if @spec_version
|
|
37
|
-
npx_command += ["--verbose"] if @verbose
|
|
38
|
-
npx_command += ["--expected-failures", expected_failures_yml]
|
|
39
|
-
npx_command
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def start_server
|
|
43
|
-
puts "Starting conformance server on port #{@port}..."
|
|
44
|
-
|
|
45
|
-
server_pid = fork do
|
|
46
|
-
Conformance::Server.new(port: @port).start
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
health_url = URI("http://localhost:#{@port}/health")
|
|
50
|
-
ready = false
|
|
51
|
-
SERVER_HEALTH_CHECK_RETRIES.times do
|
|
52
|
-
begin
|
|
53
|
-
response = Net::HTTP.get_response(health_url)
|
|
54
|
-
if response.code == "200"
|
|
55
|
-
ready = true
|
|
56
|
-
break
|
|
57
|
-
end
|
|
58
|
-
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout
|
|
59
|
-
# not ready yet
|
|
60
|
-
end
|
|
61
|
-
sleep(SERVER_POLL_INTERVAL)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
unless ready
|
|
65
|
-
warn("ERROR: Conformance server did not start within #{SERVER_START_TIMEOUT} seconds")
|
|
66
|
-
terminate_server(server_pid)
|
|
67
|
-
exit(1)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
puts "Server ready. Running conformance tests..."
|
|
71
|
-
|
|
72
|
-
server_pid
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def run_conformance(command, server_pid:)
|
|
76
|
-
puts "Command: #{command.join(" ")}\n\n"
|
|
77
|
-
|
|
78
|
-
conformance_exit_code = nil
|
|
79
|
-
begin
|
|
80
|
-
system(*command)
|
|
81
|
-
conformance_exit_code = $CHILD_STATUS.exitstatus
|
|
82
|
-
ensure
|
|
83
|
-
terminate_server(server_pid)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
exit(conformance_exit_code || 1)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def terminate_server(pid)
|
|
90
|
-
Process.kill("TERM", pid)
|
|
91
|
-
rescue Errno::ESRCH
|
|
92
|
-
# process already exited
|
|
93
|
-
ensure
|
|
94
|
-
begin
|
|
95
|
-
Process.wait(pid)
|
|
96
|
-
rescue Errno::ECHILD
|
|
97
|
-
# already reaped
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
data/conformance/server.rb
DELETED
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rackup"
|
|
4
|
-
require "json"
|
|
5
|
-
require "uri"
|
|
6
|
-
require_relative "../lib/mcp"
|
|
7
|
-
|
|
8
|
-
module Conformance
|
|
9
|
-
# 1x1 red PNG pixel (matches TypeScript SDK and Python SDK)
|
|
10
|
-
BASE64_1X1_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
|
|
11
|
-
|
|
12
|
-
# Minimal WAV file (matches TypeScript SDK and Python SDK)
|
|
13
|
-
BASE64_MINIMAL_WAV = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="
|
|
14
|
-
|
|
15
|
-
module Tools
|
|
16
|
-
class TestSimpleText < MCP::Tool
|
|
17
|
-
tool_name "test_simple_text"
|
|
18
|
-
description "A tool that returns simple text content"
|
|
19
|
-
|
|
20
|
-
class << self
|
|
21
|
-
def call(**_args)
|
|
22
|
-
MCP::Tool::Response.new([MCP::Content::Text.new("This is a simple text response for testing.").to_h])
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
class TestImageContent < MCP::Tool
|
|
28
|
-
tool_name "test_image_content"
|
|
29
|
-
description "A tool that returns image content"
|
|
30
|
-
|
|
31
|
-
class << self
|
|
32
|
-
def call(**_args)
|
|
33
|
-
MCP::Tool::Response.new([MCP::Content::Image.new(BASE64_1X1_PNG, "image/png").to_h])
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
class TestAudioContent < MCP::Tool
|
|
39
|
-
tool_name "test_audio_content"
|
|
40
|
-
description "A tool that returns audio content"
|
|
41
|
-
|
|
42
|
-
class << self
|
|
43
|
-
def call(**_args)
|
|
44
|
-
MCP::Tool::Response.new([MCP::Content::Audio.new(BASE64_MINIMAL_WAV, "audio/wav").to_h])
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class TestEmbeddedResource < MCP::Tool
|
|
50
|
-
tool_name "test_embedded_resource"
|
|
51
|
-
description "A tool that returns embedded resource content"
|
|
52
|
-
|
|
53
|
-
class << self
|
|
54
|
-
def call(**_args)
|
|
55
|
-
text_contents = MCP::Resource::TextContents.new(
|
|
56
|
-
uri: "test://embedded-resource",
|
|
57
|
-
mime_type: "text/plain",
|
|
58
|
-
text: "This is an embedded resource content.",
|
|
59
|
-
)
|
|
60
|
-
MCP::Tool::Response.new([MCP::Content::EmbeddedResource.new(text_contents).to_h])
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
class TestMultipleContentTypes < MCP::Tool
|
|
66
|
-
tool_name "test_multiple_content_types"
|
|
67
|
-
description "A tool that returns multiple content types"
|
|
68
|
-
|
|
69
|
-
class << self
|
|
70
|
-
def call(**_args)
|
|
71
|
-
MCP::Tool::Response.new([
|
|
72
|
-
MCP::Content::Text.new("Multiple content types test:").to_h,
|
|
73
|
-
MCP::Content::Image.new(BASE64_1X1_PNG, "image/png").to_h,
|
|
74
|
-
MCP::Content::EmbeddedResource.new(
|
|
75
|
-
MCP::Resource::TextContents.new(
|
|
76
|
-
uri: "test://mixed-content-resource",
|
|
77
|
-
mime_type: "application/json",
|
|
78
|
-
text: '{"test":"data","value":123}',
|
|
79
|
-
),
|
|
80
|
-
).to_h,
|
|
81
|
-
])
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
class TestErrorHandling < MCP::Tool
|
|
87
|
-
tool_name "test_error_handling"
|
|
88
|
-
description "A tool that intentionally returns an error for testing"
|
|
89
|
-
|
|
90
|
-
class << self
|
|
91
|
-
def call(**_args)
|
|
92
|
-
MCP::Tool::Response.new(
|
|
93
|
-
[MCP::Content::Text.new("This tool intentionally returns an error for testing").to_h],
|
|
94
|
-
error: true,
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
class JsonSchema202012Tool < MCP::Tool
|
|
101
|
-
tool_name "json_schema_2020_12_tool"
|
|
102
|
-
description "Tool with JSON Schema 2020-12 features"
|
|
103
|
-
input_schema(
|
|
104
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
105
|
-
"$defs": {
|
|
106
|
-
address: {
|
|
107
|
-
type: "object",
|
|
108
|
-
properties: {
|
|
109
|
-
street: { type: "string" },
|
|
110
|
-
city: { type: "string" },
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
properties: {
|
|
115
|
-
name: { type: "string" },
|
|
116
|
-
address: { "$ref": "#/$defs/address" },
|
|
117
|
-
},
|
|
118
|
-
additionalProperties: false,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
class << self
|
|
122
|
-
def call(**_args)
|
|
123
|
-
MCP::Tool::Response.new([MCP::Content::Text.new("Processed with JSON Schema 2020-12").to_h])
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
class TestToolWithLogging < MCP::Tool
|
|
129
|
-
tool_name "test_tool_with_logging"
|
|
130
|
-
description "A tool that sends log messages during execution"
|
|
131
|
-
|
|
132
|
-
class << self
|
|
133
|
-
def call(server_context:, **_args)
|
|
134
|
-
server_context.notify_log_message(data: "Tool execution started", level: "info", logger: "test_logger")
|
|
135
|
-
sleep(0.05) # Required by the conformance test to verify clients handle interleaved notifications (same as TypeScript SDK).
|
|
136
|
-
server_context.notify_log_message(data: "Tool processing data", level: "info", logger: "test_logger")
|
|
137
|
-
sleep(0.05) # Same as above.
|
|
138
|
-
server_context.notify_log_message(data: "Tool execution completed", level: "info", logger: "test_logger")
|
|
139
|
-
MCP::Tool::Response.new([MCP::Content::Text.new("Logging complete (3 messages sent)").to_h])
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# test_tool_with_progress: the actual progress dispatch is in `tools_call_handler`
|
|
145
|
-
class TestToolWithProgress < MCP::Tool
|
|
146
|
-
tool_name "test_tool_with_progress"
|
|
147
|
-
description "A tool that reports progress notifications"
|
|
148
|
-
|
|
149
|
-
class << self
|
|
150
|
-
def call(**_args)
|
|
151
|
-
MCP::Tool::Response.new([MCP::Content::Text.new("Progress complete").to_h])
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# TODO: Implement when `Transport` supports server-to-client requests.
|
|
157
|
-
class TestSampling < MCP::Tool
|
|
158
|
-
tool_name "test_sampling"
|
|
159
|
-
description "A tool that requests LLM sampling from the client"
|
|
160
|
-
input_schema(
|
|
161
|
-
properties: { prompt: { type: "string" } },
|
|
162
|
-
required: ["prompt"],
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
class << self
|
|
166
|
-
def call(prompt:)
|
|
167
|
-
MCP::Tool::Response.new(
|
|
168
|
-
[MCP::Content::Text.new("Sampling not supported in this SDK version").to_h],
|
|
169
|
-
error: true,
|
|
170
|
-
)
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# TODO: Implement when `Transport` supports server-to-client requests.
|
|
176
|
-
class TestElicitation < MCP::Tool
|
|
177
|
-
tool_name "test_elicitation"
|
|
178
|
-
description "A tool that requests user input from the client"
|
|
179
|
-
input_schema(
|
|
180
|
-
properties: { message: { type: "string" } },
|
|
181
|
-
required: ["message"],
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
class << self
|
|
185
|
-
def call(message:)
|
|
186
|
-
MCP::Tool::Response.new(
|
|
187
|
-
[MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h],
|
|
188
|
-
error: true,
|
|
189
|
-
)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# TODO: Implement when `Transport` supports server-to-client requests.
|
|
195
|
-
class TestElicitationSep1034Defaults < MCP::Tool
|
|
196
|
-
tool_name "test_elicitation_sep1034_defaults"
|
|
197
|
-
description "A tool that tests elicitation with default values"
|
|
198
|
-
|
|
199
|
-
class << self
|
|
200
|
-
def call(**_args)
|
|
201
|
-
MCP::Tool::Response.new(
|
|
202
|
-
[MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h],
|
|
203
|
-
error: true,
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# TODO: Implement when `Transport` supports server-to-client requests.
|
|
210
|
-
class TestElicitationSep1330Enums < MCP::Tool
|
|
211
|
-
tool_name "test_elicitation_sep1330_enums"
|
|
212
|
-
description "A tool that tests elicitation with enum schemas"
|
|
213
|
-
|
|
214
|
-
class << self
|
|
215
|
-
def call(**_args)
|
|
216
|
-
MCP::Tool::Response.new(
|
|
217
|
-
[MCP::Content::Text.new("Elicitation not supported in this SDK version").to_h],
|
|
218
|
-
error: true,
|
|
219
|
-
)
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
class TestReconnection < MCP::Tool
|
|
225
|
-
tool_name "test_reconnection"
|
|
226
|
-
description "A tool that triggers SSE stream closure to test client reconnection behavior"
|
|
227
|
-
|
|
228
|
-
class << self
|
|
229
|
-
def call(**_args)
|
|
230
|
-
MCP::Tool::Response.new([MCP::Content::Text.new("Reconnection test completed").to_h])
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
module Prompts
|
|
237
|
-
class TestSimplePrompt < MCP::Prompt
|
|
238
|
-
prompt_name "test_simple_prompt"
|
|
239
|
-
description "A simple prompt for testing with no arguments"
|
|
240
|
-
|
|
241
|
-
class << self
|
|
242
|
-
def template(_args, server_context: nil)
|
|
243
|
-
MCP::Prompt::Result.new(
|
|
244
|
-
messages: [
|
|
245
|
-
MCP::Prompt::Message.new(
|
|
246
|
-
role: "user",
|
|
247
|
-
content: MCP::Content::Text.new("This is a simple prompt for testing."),
|
|
248
|
-
),
|
|
249
|
-
],
|
|
250
|
-
)
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
class TestPromptWithArguments < MCP::Prompt
|
|
256
|
-
prompt_name "test_prompt_with_arguments"
|
|
257
|
-
description "A prompt with required arguments for testing"
|
|
258
|
-
arguments [
|
|
259
|
-
MCP::Prompt::Argument.new(name: "arg1", description: "First test argument", required: true),
|
|
260
|
-
MCP::Prompt::Argument.new(name: "arg2", description: "Second test argument", required: true),
|
|
261
|
-
]
|
|
262
|
-
|
|
263
|
-
class << self
|
|
264
|
-
def template(args, server_context: nil)
|
|
265
|
-
arg1 = args.dig(:arg1) || args.dig("arg1") || ""
|
|
266
|
-
arg2 = args.dig(:arg2) || args.dig("arg2") || ""
|
|
267
|
-
MCP::Prompt::Result.new(
|
|
268
|
-
messages: [
|
|
269
|
-
MCP::Prompt::Message.new(
|
|
270
|
-
role: "user",
|
|
271
|
-
content: MCP::Content::Text.new("Prompt with arguments: arg1='#{arg1}', arg2='#{arg2}'"),
|
|
272
|
-
),
|
|
273
|
-
],
|
|
274
|
-
)
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
class TestPromptWithEmbeddedResource < MCP::Prompt
|
|
280
|
-
prompt_name "test_prompt_with_embedded_resource"
|
|
281
|
-
description "A prompt with an embedded resource for testing"
|
|
282
|
-
arguments [
|
|
283
|
-
MCP::Prompt::Argument.new(name: "resourceUri", description: "URI of the resource to embed", required: true),
|
|
284
|
-
]
|
|
285
|
-
|
|
286
|
-
class << self
|
|
287
|
-
def template(args, server_context: nil)
|
|
288
|
-
resource_uri = args.dig(:resourceUri) || args.dig("resourceUri") || "test://example-resource"
|
|
289
|
-
MCP::Prompt::Result.new(
|
|
290
|
-
messages: [
|
|
291
|
-
MCP::Prompt::Message.new(
|
|
292
|
-
role: "user",
|
|
293
|
-
content: MCP::Content::EmbeddedResource.new(
|
|
294
|
-
MCP::Resource::TextContents.new(
|
|
295
|
-
uri: resource_uri,
|
|
296
|
-
mime_type: "text/plain",
|
|
297
|
-
text: "Embedded resource content for testing.",
|
|
298
|
-
),
|
|
299
|
-
),
|
|
300
|
-
),
|
|
301
|
-
MCP::Prompt::Message.new(
|
|
302
|
-
role: "user",
|
|
303
|
-
content: MCP::Content::Text.new("Please process the embedded resource above."),
|
|
304
|
-
),
|
|
305
|
-
],
|
|
306
|
-
)
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
class TestPromptWithImage < MCP::Prompt
|
|
312
|
-
prompt_name "test_prompt_with_image"
|
|
313
|
-
description "A prompt with image content for testing"
|
|
314
|
-
|
|
315
|
-
class << self
|
|
316
|
-
def template(_args, server_context: nil)
|
|
317
|
-
MCP::Prompt::Result.new(
|
|
318
|
-
messages: [
|
|
319
|
-
MCP::Prompt::Message.new(
|
|
320
|
-
role: "user",
|
|
321
|
-
content: MCP::Content::Image.new(BASE64_1X1_PNG, "image/png"),
|
|
322
|
-
),
|
|
323
|
-
MCP::Prompt::Message.new(
|
|
324
|
-
role: "user",
|
|
325
|
-
content: MCP::Content::Text.new("Please analyze the image above."),
|
|
326
|
-
),
|
|
327
|
-
],
|
|
328
|
-
)
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
class Server
|
|
335
|
-
DEFAULT_PORT = 9292
|
|
336
|
-
|
|
337
|
-
class DnsRebindingProtection
|
|
338
|
-
LOCALHOST_PATTERNS = /\A(localhost|127\.0\.0\.1|\[::1\]|::1)(:\d+)?\z/i.freeze
|
|
339
|
-
|
|
340
|
-
def initialize(app)
|
|
341
|
-
@app = app
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def call(env)
|
|
345
|
-
host = env["HTTP_HOST"] || env["SERVER_NAME"] || ""
|
|
346
|
-
|
|
347
|
-
unless localhost?(host)
|
|
348
|
-
return [
|
|
349
|
-
403,
|
|
350
|
-
{ "Content-Type" => "application/json" },
|
|
351
|
-
[{ error: "Forbidden: DNS rebinding protection - invalid Host header '#{host}'" }.to_json],
|
|
352
|
-
]
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
origin = env["HTTP_ORIGIN"]
|
|
356
|
-
if origin && !origin.empty?
|
|
357
|
-
begin
|
|
358
|
-
origin_host = URI.parse(origin).host.to_s
|
|
359
|
-
unless localhost?(origin_host)
|
|
360
|
-
return [
|
|
361
|
-
403,
|
|
362
|
-
{ "Content-Type" => "application/json" },
|
|
363
|
-
[{ error: "Forbidden: DNS rebinding protection - invalid Origin '#{origin}'" }.to_json],
|
|
364
|
-
]
|
|
365
|
-
end
|
|
366
|
-
rescue URI::InvalidURIError
|
|
367
|
-
return [
|
|
368
|
-
403,
|
|
369
|
-
{ "Content-Type" => "application/json" },
|
|
370
|
-
[{ error: "Forbidden: invalid Origin header" }.to_json],
|
|
371
|
-
]
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
@app.call(env)
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
private
|
|
379
|
-
|
|
380
|
-
def localhost?(host)
|
|
381
|
-
host.empty? || host.match?(LOCALHOST_PATTERNS)
|
|
382
|
-
end
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def initialize(port: DEFAULT_PORT)
|
|
386
|
-
@port = port
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def start
|
|
390
|
-
server = build_server
|
|
391
|
-
transport = build_transport(server)
|
|
392
|
-
configure_handlers(server)
|
|
393
|
-
rack_app = build_rack_app(transport)
|
|
394
|
-
|
|
395
|
-
puts <<~MESSAGE
|
|
396
|
-
MCP Conformance Server starting on http://localhost:#{@port}/mcp
|
|
397
|
-
Use Ctrl-C to stop
|
|
398
|
-
MESSAGE
|
|
399
|
-
|
|
400
|
-
Rackup::Handler.get("puma").run(rack_app, Port: @port, Host: "localhost", Silent: true)
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
private
|
|
404
|
-
|
|
405
|
-
def build_server
|
|
406
|
-
MCP::Server.new(
|
|
407
|
-
name: "ruby-sdk-conformance-server",
|
|
408
|
-
version: MCP::VERSION,
|
|
409
|
-
tools: [
|
|
410
|
-
Tools::TestSimpleText,
|
|
411
|
-
Tools::TestImageContent,
|
|
412
|
-
Tools::TestAudioContent,
|
|
413
|
-
Tools::TestEmbeddedResource,
|
|
414
|
-
Tools::TestMultipleContentTypes,
|
|
415
|
-
Tools::TestErrorHandling,
|
|
416
|
-
Tools::JsonSchema202012Tool,
|
|
417
|
-
Tools::TestToolWithLogging,
|
|
418
|
-
Tools::TestToolWithProgress,
|
|
419
|
-
Tools::TestSampling,
|
|
420
|
-
Tools::TestElicitation,
|
|
421
|
-
Tools::TestElicitationSep1034Defaults,
|
|
422
|
-
Tools::TestElicitationSep1330Enums,
|
|
423
|
-
Tools::TestReconnection,
|
|
424
|
-
],
|
|
425
|
-
prompts: [
|
|
426
|
-
Prompts::TestSimplePrompt,
|
|
427
|
-
Prompts::TestPromptWithArguments,
|
|
428
|
-
Prompts::TestPromptWithEmbeddedResource,
|
|
429
|
-
Prompts::TestPromptWithImage,
|
|
430
|
-
],
|
|
431
|
-
resources: resources,
|
|
432
|
-
resource_templates: resource_templates,
|
|
433
|
-
capabilities: {
|
|
434
|
-
tools: { listChanged: true },
|
|
435
|
-
prompts: { listChanged: true },
|
|
436
|
-
resources: { listChanged: true, subscribe: true },
|
|
437
|
-
logging: {},
|
|
438
|
-
completions: {},
|
|
439
|
-
},
|
|
440
|
-
)
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def resources
|
|
444
|
-
[
|
|
445
|
-
MCP::Resource.new(
|
|
446
|
-
uri: "test://static-text",
|
|
447
|
-
name: "static-text",
|
|
448
|
-
description: "A static text resource for testing",
|
|
449
|
-
mime_type: "text/plain",
|
|
450
|
-
),
|
|
451
|
-
MCP::Resource.new(
|
|
452
|
-
uri: "test://static-binary",
|
|
453
|
-
name: "static-binary",
|
|
454
|
-
description: "A static binary (PNG) resource for testing",
|
|
455
|
-
mime_type: "image/png",
|
|
456
|
-
),
|
|
457
|
-
MCP::Resource.new(
|
|
458
|
-
uri: "test://watched-resource",
|
|
459
|
-
name: "watched-resource",
|
|
460
|
-
description: "A resource for subscription testing",
|
|
461
|
-
mime_type: "text/plain",
|
|
462
|
-
),
|
|
463
|
-
]
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def resource_templates
|
|
467
|
-
[
|
|
468
|
-
MCP::ResourceTemplate.new(
|
|
469
|
-
uri_template: "test://template/{id}/data",
|
|
470
|
-
name: "template-resource",
|
|
471
|
-
description: "A parameterized resource template for testing",
|
|
472
|
-
mime_type: "application/json",
|
|
473
|
-
),
|
|
474
|
-
]
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
def build_transport(server)
|
|
478
|
-
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
479
|
-
server.transport = transport
|
|
480
|
-
transport
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
def configure_handlers(server)
|
|
484
|
-
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "debug")
|
|
485
|
-
server.server_context = server
|
|
486
|
-
|
|
487
|
-
configure_resources_read_handler(server)
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
def configure_resources_read_handler(server)
|
|
491
|
-
server.resources_read_handler do |params|
|
|
492
|
-
uri = params[:uri].to_s
|
|
493
|
-
|
|
494
|
-
case uri
|
|
495
|
-
when "test://static-text"
|
|
496
|
-
[
|
|
497
|
-
MCP::Resource::TextContents.new(
|
|
498
|
-
text: "This is the content of the static text resource.",
|
|
499
|
-
uri: uri,
|
|
500
|
-
mime_type: "text/plain",
|
|
501
|
-
).to_h,
|
|
502
|
-
]
|
|
503
|
-
when "test://static-binary"
|
|
504
|
-
[
|
|
505
|
-
MCP::Resource::BlobContents.new(
|
|
506
|
-
data: BASE64_1X1_PNG,
|
|
507
|
-
uri: uri,
|
|
508
|
-
mime_type: "image/png",
|
|
509
|
-
).to_h,
|
|
510
|
-
]
|
|
511
|
-
when %r{\Atest://template/(.+)/data\z}
|
|
512
|
-
id = Regexp.last_match(1)
|
|
513
|
-
content = { id: id, templateTest: true, data: "Data for ID: #{id}" }.to_json
|
|
514
|
-
|
|
515
|
-
[
|
|
516
|
-
MCP::Resource::TextContents.new(
|
|
517
|
-
text: content,
|
|
518
|
-
uri: uri,
|
|
519
|
-
mime_type: "application/json",
|
|
520
|
-
).to_h,
|
|
521
|
-
]
|
|
522
|
-
else
|
|
523
|
-
[]
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
def build_rack_app(transport)
|
|
529
|
-
mcp_app = proc do |env|
|
|
530
|
-
request = Rack::Request.new(env)
|
|
531
|
-
|
|
532
|
-
if request.path_info == "/health"
|
|
533
|
-
[200, { "Content-Type" => "application/json" }, ['{"status":"ok"}']]
|
|
534
|
-
elsif request.path_info == "/mcp" || request.path_info == "/"
|
|
535
|
-
transport.handle_request(request)
|
|
536
|
-
else
|
|
537
|
-
[404, { "Content-Type" => "application/json" }, ['{"error":"Not found"}']]
|
|
538
|
-
end
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
Rack::Builder.new do
|
|
542
|
-
use(DnsRebindingProtection)
|
|
543
|
-
run(mcp_app)
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
end
|
data/dev.yml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: mcp-ruby
|
|
2
|
-
|
|
3
|
-
type: ruby
|
|
4
|
-
|
|
5
|
-
up:
|
|
6
|
-
- ruby
|
|
7
|
-
- bundler
|
|
8
|
-
|
|
9
|
-
commands:
|
|
10
|
-
console:
|
|
11
|
-
desc: Open console with the gem loaded
|
|
12
|
-
run: bin/console
|
|
13
|
-
build:
|
|
14
|
-
desc: Build the gem using rake build
|
|
15
|
-
run: bin/rake build
|
|
16
|
-
test:
|
|
17
|
-
desc: Run tests
|
|
18
|
-
syntax:
|
|
19
|
-
argument: file
|
|
20
|
-
optional: args...
|
|
21
|
-
run: |
|
|
22
|
-
if [[ $# -eq 0 ]]; then
|
|
23
|
-
bin/rake test
|
|
24
|
-
else
|
|
25
|
-
bin/rake -I test "$@"
|
|
26
|
-
fi
|
|
27
|
-
style:
|
|
28
|
-
desc: Run rubocop
|
|
29
|
-
aliases: [rubocop, lint]
|
|
30
|
-
run: bin/rake rubocop
|
data/docs/_config.yml
DELETED