bundleup-sdk 0.5.0 â 0.6.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 +19 -68
- data/lib/bundleup/mcp.rb +4 -206
- data/lib/bundleup/unify.rb +0 -5
- data/lib/bundleup/version.rb +1 -1
- data/lib/bundleup.rb +0 -1
- metadata +2 -3
- data/lib/bundleup/unify/mcp.rb +0 -44
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9265bf377a655ed9056e02bd22d1ec24fec68304e7ad4881c99013c42ca516f2
|
|
4
|
+
data.tar.gz: 4f9dac8314ce2d26640048984f7bef53e8921dfa9d4a7be5c5857412af5d3b00
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e24b6ae2f52e09f5a2eb65e697b1080b4f8a36c5052097ea660b145fec36c7b85aef08ce85979d21023e65aa2788b4444071993e2f3ba3ea5b1f11bdbd2b48f
|
|
7
|
+
data.tar.gz: 71008902408646b7d4e171c186bc28337f0b67c7b36e218f01b2cf637402b06edf27cc2fa070dd036a35d9d428eecdad51f7935e1f2618e7dfdbd61b92b2949e
|
data/README.md
CHANGED
|
@@ -72,7 +72,7 @@ The BundleUp SDK is tested and supported on:
|
|
|
72
72
|
- ð **100+ Integrations** - Connect to Slack, GitHub, Jira, Linear, and many more
|
|
73
73
|
- ðŊ **Unified API** - Consistent interface across all integrations via Unify API
|
|
74
74
|
- ð **Proxy API** - Direct access to underlying integration APIs
|
|
75
|
-
- ðĪ **MCP** -
|
|
75
|
+
- ðĪ **MCP** - Hand a connection to any MCP client, or to OpenAI and Anthropic's hosted MCP
|
|
76
76
|
- ðŠķ **Lightweight** - Minimal dependencies
|
|
77
77
|
- ðĄïļ **Error Handling** - Comprehensive error messages and validation
|
|
78
78
|
- ð **Well Documented** - Extensive documentation and examples
|
|
@@ -1340,47 +1340,14 @@ Reach a provider's own MCP server using a connection's credentials. BundleUp inj
|
|
|
1340
1340
|
|
|
1341
1341
|
Supported for providers that run a first-party MCP server â see the [integrations page](https://www.bundleup.io/integrations). Others return an `mcp_not_supported` error.
|
|
1342
1342
|
|
|
1343
|
-
|
|
1343
|
+
BundleUp does not ship an MCP client. Hand `hosted` or `transport` to the one you already use, or send JSON-RPC yourself with `post` and `delete`, which return the response untouched as `Faraday::Response` objects, like the Proxy API.
|
|
1344
1344
|
|
|
1345
|
-
#### Creating an MCP
|
|
1345
|
+
#### Creating an MCP Instance
|
|
1346
1346
|
|
|
1347
1347
|
```ruby
|
|
1348
1348
|
mcp = client.mcp('conn_123abc')
|
|
1349
1349
|
```
|
|
1350
1350
|
|
|
1351
|
-
#### Managed Sessions
|
|
1352
|
-
|
|
1353
|
-
`connect` returns a client that handles the handshake, session ID and response decoding, and exposes what the provider offers.
|
|
1354
|
-
|
|
1355
|
-
```ruby
|
|
1356
|
-
mcp = client.mcp('conn_123abc').connect
|
|
1357
|
-
|
|
1358
|
-
tools = mcp.list_tools
|
|
1359
|
-
result = mcp.call_tool('create_issue', { title: 'Login broken' })
|
|
1360
|
-
|
|
1361
|
-
mcp.close
|
|
1362
|
-
```
|
|
1363
|
-
|
|
1364
|
-
Resources and prompts follow the same shape:
|
|
1365
|
-
|
|
1366
|
-
```ruby
|
|
1367
|
-
resources = mcp.list_resources
|
|
1368
|
-
contents = mcp.read_resource('file:///readme.md')
|
|
1369
|
-
|
|
1370
|
-
prompts = mcp.list_prompts
|
|
1371
|
-
messages = mcp.get_prompt('summarize', { id: '123' })
|
|
1372
|
-
```
|
|
1373
|
-
|
|
1374
|
-
Anything else in the protocol:
|
|
1375
|
-
|
|
1376
|
-
```ruby
|
|
1377
|
-
mcp.request('logging/setLevel', { level: 'debug' })
|
|
1378
|
-
```
|
|
1379
|
-
|
|
1380
|
-
The handshake runs lazily on the first call and once per client, list methods follow `nextCursor` to the end, and `text/event-stream` responses are decoded for you. Results are hashes with string keys. Errors raise a `RuntimeError` with the provider's message, or BundleUp's with its code appended â `Missing or invalid connection ID (connection_invalid)`.
|
|
1381
|
-
|
|
1382
|
-
Call `close` when you are done to end the session upstream.
|
|
1383
|
-
|
|
1384
1351
|
#### Model-Hosted MCP
|
|
1385
1352
|
|
|
1386
1353
|
OpenAI and Anthropic can connect to an MCP server themselves, with no tool mapping or dispatch loop on your side. Both accept only a single credential and no custom headers, so `hosted` returns the server URL alongside the API key and connection joined into one bearer.
|
|
@@ -1403,7 +1370,7 @@ response = openai.responses.create(
|
|
|
1403
1370
|
)
|
|
1404
1371
|
```
|
|
1405
1372
|
|
|
1406
|
-
Anthropic's connector takes the same pair as `url` and `authorization_token`.
|
|
1373
|
+
Anthropic's connector takes the same pair as `url` and `authorization_token`.
|
|
1407
1374
|
|
|
1408
1375
|
`server_url` must be exactly the URL `hosted` returns â the proxy rebuilds the upstream URL from the provider's own base, so any path or query you append is ignored rather than rejected.
|
|
1409
1376
|
|
|
@@ -1506,45 +1473,29 @@ end
|
|
|
1506
1473
|
|
|
1507
1474
|
Every JSON-RPC message counts toward the rate limit of 100 requests per 60 seconds, per connection â including the `initialize` handshake.
|
|
1508
1475
|
|
|
1509
|
-
####
|
|
1476
|
+
#### Several Connections
|
|
1510
1477
|
|
|
1511
|
-
An agent often needs more than one provider for the same end user.
|
|
1478
|
+
An agent often needs more than one provider for the same end user. With model-hosted MCP there is nothing to merge â pass one `mcp` tool per connection and the model provider keeps them apart by `server_label`:
|
|
1512
1479
|
|
|
1513
1480
|
```ruby
|
|
1514
|
-
|
|
1515
|
-
'slack' => client.mcp(user.slack_connection).connect,
|
|
1516
|
-
'linear' => client.mcp(user.linear_connection).connect,
|
|
1517
|
-
'crm' => client.unify(user.hubspot_connection).mcp
|
|
1518
|
-
}
|
|
1481
|
+
connections = { 'slack' => user.slack_connection, 'linear' => user.linear_connection }
|
|
1519
1482
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
mcp.list_tools.map { |tool| tool.merge('name' => "#{label}__#{tool['name']}") }
|
|
1523
|
-
end
|
|
1483
|
+
tools = connections.map do |label, connection_id|
|
|
1484
|
+
hosted = client.mcp(connection_id).hosted
|
|
1524
1485
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1486
|
+
{
|
|
1487
|
+
type: 'mcp',
|
|
1488
|
+
server_label: label,
|
|
1489
|
+
server_url: hosted[:url],
|
|
1490
|
+
authorization: hosted[:token],
|
|
1491
|
+
require_approval: 'never'
|
|
1492
|
+
}
|
|
1529
1493
|
end
|
|
1530
1494
|
```
|
|
1531
1495
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
Two things worth handling that the sketch above skips. **Filter before you hand the list to a model** â three providers is easily sixty tools, and accuracy drops as that list grows, so select the ones the agent actually needs rather than passing everything. And decide what an unreachable provider should do: as written, one failing `list_tools` raises and fails the whole list, while a `rescue` per client lets the others through.
|
|
1535
|
-
|
|
1536
|
-
#### Unified MCP
|
|
1537
|
-
|
|
1538
|
-
BundleUp's normalized tools instead of the provider's, on the same protocol. Tools only â Unified MCP exposes no resources or prompts.
|
|
1539
|
-
|
|
1540
|
-
```ruby
|
|
1541
|
-
mcp = client.unify('conn_123abc').mcp
|
|
1542
|
-
|
|
1543
|
-
tools = mcp.list_tools
|
|
1544
|
-
result = mcp.call_tool('send_message', { text: 'Deploy finished' })
|
|
1545
|
-
```
|
|
1496
|
+
With an MCP client in your own backend, open one client per connection from its `transport`, prefix each tool name with a label (`slack__send_message`), and route calls back by that prefix. There is no merge helper in the SDK â how tools are namespaced, filtered and recovered from differs enough per agent that it is better written where you can see it.
|
|
1546
1497
|
|
|
1547
|
-
|
|
1498
|
+
**Filter before you hand tools to a model** â three providers is easily sixty tools, and accuracy drops as that list grows, so give the agent only the ones it needs.
|
|
1548
1499
|
|
|
1549
1500
|
## Error Handling
|
|
1550
1501
|
|
|
@@ -1589,7 +1540,7 @@ lib/
|
|
|
1589
1540
|
â âââ client.rb # Main client class
|
|
1590
1541
|
â âââ auth.rb # Auth API (authorization URL + code exchange)
|
|
1591
1542
|
â âââ proxy.rb # Proxy API implementation
|
|
1592
|
-
â âââ mcp.rb # MCP API (transport +
|
|
1543
|
+
â âââ mcp.rb # MCP API (transport + hosted)
|
|
1593
1544
|
â âââ unify.rb # Unify API client wrapper
|
|
1594
1545
|
â âââ version.rb # Gem version
|
|
1595
1546
|
â âââ resources/
|
data/lib/bundleup/mcp.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module BundleUp
|
|
6
|
-
#
|
|
6
|
+
# A connection's MCP server.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# response
|
|
8
|
+
# BundleUp does not ship an MCP client. Hand +transport+ or +hosted+ to the
|
|
9
|
+
# client you already use, or drive the protocol yourself with +post+ and
|
|
10
|
+
# +delete+, which return the raw Faraday response the way Proxy does.
|
|
11
11
|
class MCP
|
|
12
12
|
BASE_URL = 'https://mcp.bundleup.io'
|
|
13
13
|
|
|
@@ -47,11 +47,6 @@ module BundleUp
|
|
|
47
47
|
connection.delete(BASE_URL, nil, default_headers.merge(headers))
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
# Open a managed MCP session for this connection.
|
|
51
|
-
def connect
|
|
52
|
-
MCPClient.new(BASE_URL, @api_key, @connection_id)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
50
|
private
|
|
56
51
|
|
|
57
52
|
def default_headers
|
|
@@ -67,201 +62,4 @@ module BundleUp
|
|
|
67
62
|
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
68
63
|
end
|
|
69
64
|
end
|
|
70
|
-
|
|
71
|
-
# A connected MCP session.
|
|
72
|
-
#
|
|
73
|
-
# Tools, resources and prompts are defined by the provider â BundleUp does
|
|
74
|
-
# not rename or normalize them.
|
|
75
|
-
class MCPClient
|
|
76
|
-
PROTOCOL_VERSION = '2025-06-18'
|
|
77
|
-
CLIENT_NAME = 'bundleup-sdk'
|
|
78
|
-
|
|
79
|
-
def initialize(base_url, api_key, connection_id)
|
|
80
|
-
@base_url = base_url
|
|
81
|
-
@api_key = api_key
|
|
82
|
-
@connection_id = connection_id
|
|
83
|
-
@session_id = nil
|
|
84
|
-
@connected = false
|
|
85
|
-
@last_id = 0
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# List the provider's tools, following pagination to the end.
|
|
89
|
-
def list_tools
|
|
90
|
-
paginate('tools/list', 'tools')
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Call a tool by name, with arguments matching its own input schema.
|
|
94
|
-
def call_tool(name, args = {})
|
|
95
|
-
raise ArgumentError, 'Tool name is required to call a tool.' if blank?(name)
|
|
96
|
-
|
|
97
|
-
connect
|
|
98
|
-
send_message('tools/call', { name: name, arguments: args })
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# List the provider's resources, following pagination to the end.
|
|
102
|
-
def list_resources
|
|
103
|
-
paginate('resources/list', 'resources')
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Read a resource by URI.
|
|
107
|
-
def read_resource(uri)
|
|
108
|
-
raise ArgumentError, 'Resource URI is required to read a resource.' if blank?(uri)
|
|
109
|
-
|
|
110
|
-
connect
|
|
111
|
-
send_message('resources/read', { uri: uri })
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# List the provider's prompts, following pagination to the end.
|
|
115
|
-
def list_prompts
|
|
116
|
-
paginate('prompts/list', 'prompts')
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Get a prompt by name.
|
|
120
|
-
def get_prompt(name, args = {})
|
|
121
|
-
raise ArgumentError, 'Prompt name is required to get a prompt.' if blank?(name)
|
|
122
|
-
|
|
123
|
-
connect
|
|
124
|
-
send_message('prompts/get', { name: name, arguments: args })
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Send any other JSON-RPC method on this session.
|
|
128
|
-
def request(method, params = nil)
|
|
129
|
-
raise ArgumentError, 'Method is required to send a request.' if blank?(method)
|
|
130
|
-
|
|
131
|
-
connect
|
|
132
|
-
send_message(method, params)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# End the session and reset local state.
|
|
136
|
-
def close
|
|
137
|
-
delete_session if @session_id
|
|
138
|
-
|
|
139
|
-
@session_id = nil
|
|
140
|
-
@connected = false
|
|
141
|
-
nil
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
private
|
|
145
|
-
|
|
146
|
-
def blank?(value)
|
|
147
|
-
value.nil? || value.to_s.empty?
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def default_headers
|
|
151
|
-
headers = {
|
|
152
|
-
'Authorization' => "Bearer #{@api_key}",
|
|
153
|
-
'Content-Type' => 'application/json',
|
|
154
|
-
'Accept' => 'application/json, text/event-stream',
|
|
155
|
-
'BU-Connection-Id' => @connection_id
|
|
156
|
-
}
|
|
157
|
-
headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
158
|
-
headers
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def connection
|
|
162
|
-
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def post_payload(payload)
|
|
166
|
-
response = connection.post(@base_url, payload.to_json, default_headers)
|
|
167
|
-
session_id = response.headers['mcp-session-id']
|
|
168
|
-
@session_id = session_id if session_id
|
|
169
|
-
|
|
170
|
-
raise error_for(response) unless response.success?
|
|
171
|
-
|
|
172
|
-
response
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def error_for(response)
|
|
176
|
-
fallback = "MCP request failed with status #{response.status}."
|
|
177
|
-
parsed = JSON.parse(response.body.to_s)
|
|
178
|
-
return RuntimeError.new(fallback) unless parsed.is_a?(Hash) && parsed['message']
|
|
179
|
-
|
|
180
|
-
code = parsed['code']
|
|
181
|
-
RuntimeError.new(code ? "#{parsed['message']} (#{code})" : parsed['message'])
|
|
182
|
-
rescue JSON::ParserError
|
|
183
|
-
RuntimeError.new(fallback)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Run the MCP handshake, once. Deferred until the first call.
|
|
187
|
-
def connect
|
|
188
|
-
return if @connected
|
|
189
|
-
|
|
190
|
-
send_message('initialize', handshake_params)
|
|
191
|
-
post_payload({ jsonrpc: '2.0', method: 'notifications/initialized' })
|
|
192
|
-
@connected = true
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def handshake_params
|
|
196
|
-
{
|
|
197
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
198
|
-
capabilities: {},
|
|
199
|
-
clientInfo: { name: CLIENT_NAME, version: BundleUp::VERSION }
|
|
200
|
-
}
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def send_message(method, params = nil)
|
|
204
|
-
@last_id += 1
|
|
205
|
-
payload = { jsonrpc: '2.0', id: @last_id, method: method }
|
|
206
|
-
payload[:params] = params unless params.nil?
|
|
207
|
-
|
|
208
|
-
message = parse(post_payload(payload), @last_id)
|
|
209
|
-
raise "No response received for #{method}." if message.nil?
|
|
210
|
-
raise message['error']['message'].to_s if message['error']
|
|
211
|
-
|
|
212
|
-
message['result'] || {}
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Providers may answer a plain request/response over text/event-stream.
|
|
216
|
-
def parse(response, message_id)
|
|
217
|
-
body = response.body.to_s
|
|
218
|
-
return nil if body.empty?
|
|
219
|
-
|
|
220
|
-
content_type = response.headers['content-type'].to_s
|
|
221
|
-
return JSON.parse(body) unless content_type.include?('text/event-stream')
|
|
222
|
-
|
|
223
|
-
parse_stream(body, message_id)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def parse_stream(body, message_id)
|
|
227
|
-
body.gsub("\r\n", "\n").split("\n\n").each do |event|
|
|
228
|
-
data = event_data(event)
|
|
229
|
-
next if data.empty?
|
|
230
|
-
|
|
231
|
-
message = JSON.parse(data)
|
|
232
|
-
# Skip server notifications interleaved on the stream.
|
|
233
|
-
return message if message.is_a?(Hash) && message['id'] == message_id
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
nil
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def event_data(event)
|
|
240
|
-
event.split("\n")
|
|
241
|
-
.select { |line| line.start_with?('data:') }
|
|
242
|
-
.map { |line| line.sub('data:', '').strip }
|
|
243
|
-
.join("\n")
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def paginate(method, key)
|
|
247
|
-
connect
|
|
248
|
-
items = []
|
|
249
|
-
cursor = nil
|
|
250
|
-
|
|
251
|
-
loop do
|
|
252
|
-
result = send_message(method, cursor ? { cursor: cursor } : nil)
|
|
253
|
-
items.concat(result[key] || [])
|
|
254
|
-
cursor = result['nextCursor']
|
|
255
|
-
break unless cursor
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
items
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def delete_session
|
|
262
|
-
connection.delete(@base_url, nil, default_headers)
|
|
263
|
-
rescue Faraday::Error
|
|
264
|
-
nil
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
65
|
end
|
data/lib/bundleup/unify.rb
CHANGED
|
@@ -54,11 +54,6 @@ module BundleUp
|
|
|
54
54
|
def me(params = {})
|
|
55
55
|
(@me ||= BundleUp::Unify::Me.new(api_key, connection_id)).get(params)
|
|
56
56
|
end
|
|
57
|
-
|
|
58
|
-
# Access the Unified MCP server for the connection.
|
|
59
|
-
def mcp
|
|
60
|
-
@mcp ||= BundleUp::Unify::MCP.new(api_key, connection_id)
|
|
61
|
-
end
|
|
62
57
|
end
|
|
63
58
|
end
|
|
64
59
|
end
|
data/lib/bundleup/version.rb
CHANGED
data/lib/bundleup.rb
CHANGED
|
@@ -23,7 +23,6 @@ require_relative 'bundleup/unify/ticketing'
|
|
|
23
23
|
require_relative 'bundleup/unify/crm'
|
|
24
24
|
require_relative 'bundleup/unify/drive'
|
|
25
25
|
require_relative 'bundleup/unify/calendar'
|
|
26
|
-
require_relative 'bundleup/unify/mcp'
|
|
27
26
|
require_relative 'bundleup/unify/me'
|
|
28
27
|
|
|
29
28
|
# Main module for the BundleUp SDK.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundleup-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BundleUp
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -182,7 +182,6 @@ files:
|
|
|
182
182
|
- lib/bundleup/unify/crm.rb
|
|
183
183
|
- lib/bundleup/unify/drive.rb
|
|
184
184
|
- lib/bundleup/unify/git.rb
|
|
185
|
-
- lib/bundleup/unify/mcp.rb
|
|
186
185
|
- lib/bundleup/unify/me.rb
|
|
187
186
|
- lib/bundleup/unify/ticketing.rb
|
|
188
187
|
- lib/bundleup/version.rb
|
data/lib/bundleup/unify/mcp.rb
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BundleUp
|
|
4
|
-
module Unify
|
|
5
|
-
# The Unified MCP server.
|
|
6
|
-
#
|
|
7
|
-
# Same protocol and headers as Proxy MCP, but the tools are BundleUp's
|
|
8
|
-
# normalized ones rather than the provider's. Tools only â Unified MCP
|
|
9
|
-
# exposes no resources or prompts.
|
|
10
|
-
#
|
|
11
|
-
# The server is stateless and POST-only, so there is no session to close.
|
|
12
|
-
class MCP
|
|
13
|
-
BASE_URL = 'https://unify.bundleup.io/v1/mcp'
|
|
14
|
-
|
|
15
|
-
attr_reader :api_key, :connection_id
|
|
16
|
-
|
|
17
|
-
def initialize(api_key, connection_id)
|
|
18
|
-
@api_key = api_key
|
|
19
|
-
@connection_id = connection_id
|
|
20
|
-
@client = ::BundleUp::MCPClient.new(BASE_URL, api_key, connection_id)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# The URL and a single bearer token carrying both the API key and the
|
|
24
|
-
# connection, for model-hosted MCP clients that cannot set headers.
|
|
25
|
-
def hosted
|
|
26
|
-
separator = ::BundleUp::MCP::CREDENTIAL_SEPARATOR
|
|
27
|
-
|
|
28
|
-
{ url: BASE_URL, token: "#{@api_key}#{separator}#{@connection_id}" }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# List the available unified tools.
|
|
32
|
-
def list_tools
|
|
33
|
-
@client.list_tools
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Call a unified tool with optional arguments.
|
|
37
|
-
def call_tool(name, args = {})
|
|
38
|
-
raise ArgumentError, 'Tool name is required to call a tool.' if name.nil? || name.to_s.empty?
|
|
39
|
-
|
|
40
|
-
@client.call_tool(name, args)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|