-
Notifications
You must be signed in to change notification settings - Fork 108
Expand file tree
/
Copy pathhttp_server.rb
More file actions
183 lines (159 loc) · 4.77 KB
/
http_server.rb
File metadata and controls
183 lines (159 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# frozen_string_literal: true
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "mcp"
require "rack/cors"
require "rackup"
require "json"
require "logger"
# Create a simple tool
class ExampleTool < MCP::Tool
description "A simple example tool that adds two numbers"
input_schema(
properties: {
a: { type: "number" },
b: { type: "number" },
},
required: ["a", "b"],
)
class << self
def call(a:, b:)
MCP::Tool::Response.new([{
type: "text",
text: "The sum of #{a} and #{b} is #{a + b}",
}])
end
end
end
# Create a simple prompt
class ExamplePrompt < MCP::Prompt
description "A simple example prompt that echoes back its arguments"
arguments [
MCP::Prompt::Argument.new(
name: "message",
description: "The message to echo back",
required: true,
),
]
class << self
def template(args, server_context:)
MCP::Prompt::Result.new(
messages: [
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Text.new(args[:message]),
),
],
)
end
end
end
# Set up the server
server = MCP::Server.new(
name: "example_http_server",
tools: [ExampleTool],
prompts: [ExamplePrompt],
resources: [
MCP::Resource.new(
uri: "https://test_resource.invalid",
name: "test-resource",
title: "Test Resource",
description: "Test resource that echoes back the uri as its content",
mime_type: "text/plain",
),
],
)
server.define_tool(
name: "echo",
description: "A simple example tool that echoes back its arguments",
input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
) do |message:|
MCP::Tool::Response.new(
[
{
type: "text",
text: "Hello from echo tool! Message: #{message}",
},
],
)
end
server.resources_read_handler do |params|
[{
uri: params[:uri],
mimeType: "text/plain",
text: "Hello from HTTP server resource!",
}]
end
# Create the Streamable HTTP transport
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport
# Rack middleware for MCP-specific request/response logging.
class McpRequestLogger
def initialize(app)
@app = app
@logger = Logger.new($stdout)
@logger.formatter = proc { |_severity, _datetime, _progname, msg| "[MCP] #{msg}\n" }
end
def call(env)
if env["REQUEST_METHOD"] == "POST"
body = env["rack.input"].read
env["rack.input"].rewind
begin
parsed = JSON.parse(body)
@logger.info("Request: #{parsed["method"]} (id: #{parsed["id"]})")
@logger.debug("Request body: #{JSON.pretty_generate(parsed)}")
rescue JSON::ParserError
@logger.warn("Request body (raw): #{body}")
end
end
status, headers, response_body = @app.call(env)
if response_body.is_a?(Array) && !response_body.empty? && response_body.first
begin
parsed = JSON.parse(response_body.first)
if parsed["error"]
@logger.error("Response error: #{parsed["error"]["message"]}")
else
@logger.info("Response: #{parsed["result"] ? "success" : "empty"} (id: #{parsed["id"]})")
end
@logger.debug("Response body: #{JSON.pretty_generate(parsed)}")
rescue JSON::ParserError
@logger.warn("Response body (raw): #{response_body}")
end
end
[status, headers, response_body]
end
end
# Build the Rack application with middleware.
# `StreamableHTTPTransport` responds to `call(env)`, so it can be used directly as a Rack app.
rack_app = Rack::Builder.new do
# Enable CORS to allow browser-based MCP clients (e.g., MCP Inspector)
# WARNING: origins("*") allows all origins. Restrict this in production.
use(Rack::Cors) do
allow do
origins("*")
resource(
"*",
headers: :any,
methods: [:get, :post, :delete, :options],
expose: ["Mcp-Session-Id"],
)
end
end
# Use CommonLogger for standard HTTP request logging
use(Rack::CommonLogger, Logger.new($stdout))
use(Rack::ShowExceptions)
use(McpRequestLogger)
run(transport)
end
# Start the server
puts <<~MESSAGE
Starting MCP HTTP server on http://localhost:9292
Use POST requests to initialize and send JSON-RPC commands
Example initialization:
curl -i http://localhost:9292 --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
The server will return a session ID in the Mcp-Session-Id header.
Use this session ID for subsequent requests.
Press Ctrl+C to stop the server
MESSAGE
# Run the server
# Use Rackup to run the server
Rackup::Handler.get("puma").run(rack_app, Port: 9292, Host: "localhost")