bundleup-sdk 0.4.1 → 0.4.2
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 +1 -1
- data/lib/bundleup/mcp.rb +197 -1
- data/lib/bundleup/unify/mcp.rb +4 -4
- data/lib/bundleup/unify/me.rb +27 -0
- data/lib/bundleup/unify.rb +8 -0
- data/lib/bundleup/version.rb +1 -1
- data/lib/bundleup.rb +1 -0
- data/sig/bundleup/unify/me.rbs +14 -0
- data/sig/bundleup/unify.rbs +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e98412b5b8723a8137da05f6996554d0247bfa7947e2d9ec3955a1656cb1d386
|
|
4
|
+
data.tar.gz: 8639a58d6d076123db499bfc30bababe63374821189e586aeaaaf415c23192ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b46029fe10adf813158115754593ed3d0e64ac44ce1ddc31c648a40a8580e35e93944a8701599ac777864ab0f605a0e014197b06af9830190cff01c76bb31602
|
|
7
|
+
data.tar.gz: 49d99e240a23e1e3ca9e961b1b7a06b9383bfb71e5a27cf552755db0d20ef1f2f42939c6ffbe58273ea630ad86c25d2f9e0c3dacb9d7bad7bb7054f4918f7b58
|
data/README.md
CHANGED
|
@@ -146,7 +146,7 @@ client = BundleUp::Client.new(ENV['BUNDLEUP_API_KEY'])
|
|
|
146
146
|
**Example `.env` file:**
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
|
-
BUNDLEUP_API_KEY=
|
|
149
|
+
BUNDLEUP_API_KEY=Zw7Lt7JacsDyMCEpnZdGptgnJaOdMzFVH9QtIthnL5RviYP5WeH6e9FWP2HzEO
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
**Loading environment variables (using dotenv):**
|
data/lib/bundleup/mcp.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require_relative 'mcp_client'
|
|
5
4
|
|
|
6
5
|
module BundleUp
|
|
7
6
|
# Transport for a connection's MCP server.
|
|
@@ -68,4 +67,201 @@ module BundleUp
|
|
|
68
67
|
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
69
68
|
end
|
|
70
69
|
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
|
|
71
267
|
end
|
data/lib/bundleup/unify/mcp.rb
CHANGED
|
@@ -29,15 +29,15 @@ module BundleUp
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# List the available unified tools.
|
|
32
|
-
def
|
|
33
|
-
@client.
|
|
32
|
+
def list_tools
|
|
33
|
+
@client.list_tools
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Call a unified tool with optional arguments.
|
|
37
|
-
def
|
|
37
|
+
def call_tool(name, args = {})
|
|
38
38
|
raise ArgumentError, 'Tool name is required to call a tool.' if name.nil? || name.to_s.empty?
|
|
39
39
|
|
|
40
|
-
@client.
|
|
40
|
+
@client.call_tool(name, args)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BundleUp
|
|
4
|
+
module Unify
|
|
5
|
+
# Client for the root-level identity endpoint.
|
|
6
|
+
#
|
|
7
|
+
# `me` is the one unified method every provider implements, so the API mounts
|
|
8
|
+
# it at the root rather than under a vertical. It is exposed on the Unify
|
|
9
|
+
# client as `unify.me` rather than as a namespace.
|
|
10
|
+
class Me < Base
|
|
11
|
+
# Fetches the account the connection is authenticated as.
|
|
12
|
+
#
|
|
13
|
+
# Providers that authorize per workspace, portal, tenant or company return
|
|
14
|
+
# that account instead of a user, and fields the provider does not expose
|
|
15
|
+
# come back as nil.
|
|
16
|
+
def get(params = {})
|
|
17
|
+
response = connection.get('me') do |req|
|
|
18
|
+
req.params = params
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
raise "Failed to fetch me: #{response.status}" unless response.success?
|
|
22
|
+
|
|
23
|
+
response.body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/bundleup/unify.rb
CHANGED
|
@@ -41,6 +41,14 @@ module BundleUp
|
|
|
41
41
|
@drive ||= BundleUp::Unify::Drive.new(api_key, connection_id)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Fetch the account this connection is authenticated as.
|
|
45
|
+
#
|
|
46
|
+
# `me` is the one unified method every provider implements, so it hangs off
|
|
47
|
+
# the Unify client directly instead of a vertical namespace.
|
|
48
|
+
def me(params = {})
|
|
49
|
+
(@me ||= BundleUp::Unify::Me.new(api_key, connection_id)).get(params)
|
|
50
|
+
end
|
|
51
|
+
|
|
44
52
|
# Access the Unified MCP server for the connection.
|
|
45
53
|
def mcp
|
|
46
54
|
@mcp ||= BundleUp::Unify::MCP.new(api_key, connection_id)
|
data/lib/bundleup/version.rb
CHANGED
data/lib/bundleup.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative 'bundleup/unify/ticketing'
|
|
|
22
22
|
require_relative 'bundleup/unify/crm'
|
|
23
23
|
require_relative 'bundleup/unify/drive'
|
|
24
24
|
require_relative 'bundleup/unify/mcp'
|
|
25
|
+
require_relative 'bundleup/unify/me'
|
|
25
26
|
|
|
26
27
|
# Main module for the BundleUp SDK.
|
|
27
28
|
module BundleUp
|
data/sig/bundleup/unify.rbs
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundleup-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BundleUp
|
|
@@ -182,6 +182,7 @@ files:
|
|
|
182
182
|
- lib/bundleup/unify/drive.rb
|
|
183
183
|
- lib/bundleup/unify/git.rb
|
|
184
184
|
- lib/bundleup/unify/mcp.rb
|
|
185
|
+
- lib/bundleup/unify/me.rb
|
|
185
186
|
- lib/bundleup/unify/ticketing.rb
|
|
186
187
|
- lib/bundleup/version.rb
|
|
187
188
|
- sig/bundleup/unify.rbs
|
|
@@ -190,6 +191,7 @@ files:
|
|
|
190
191
|
- sig/bundleup/unify/crm.rbs
|
|
191
192
|
- sig/bundleup/unify/drive.rbs
|
|
192
193
|
- sig/bundleup/unify/git.rbs
|
|
194
|
+
- sig/bundleup/unify/me.rbs
|
|
193
195
|
- sig/bundleup/unify/ticketing.rbs
|
|
194
196
|
homepage: https://github.com/bundleup/bundleup-sdk-ruby
|
|
195
197
|
licenses:
|