bundleup-sdk 0.3.0 → 0.4.1
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/lib/bundleup/client.rb +8 -0
- data/lib/bundleup/mcp.rb +71 -0
- data/lib/bundleup/mcp_client.rb +202 -0
- data/lib/bundleup/unify/git.rb +29 -0
- data/lib/bundleup/unify/mcp.rb +44 -0
- data/lib/bundleup/unify.rb +5 -0
- data/lib/bundleup/version.rb +1 -1
- data/lib/bundleup.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e2898652fed3548927b24223483a9308f826e2899503c4eb711275d9418145a
|
|
4
|
+
data.tar.gz: ec2179acf284beb8c8e94a1079c671ee2a3a5218905d9187648b62120bcbcaa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bf6f9e0912c4cefbc510011e4475743517c21fe324c428ccb8e2b44a24104c2fa3299069edcafca8cf1c47879d6620792fada35af230414f34e138dd5742990
|
|
7
|
+
data.tar.gz: 5dce15658d4a193bc89771b8726a1e5faea4326f7c20c76ed7c1b23a9e4eef01c1bf30e7ec785a2f478e2610fc193078f8c1d0adca4ecda0557a7846d6b07963
|
data/lib/bundleup/client.rb
CHANGED
|
@@ -38,5 +38,13 @@ module BundleUp
|
|
|
38
38
|
|
|
39
39
|
BundleUp::Unify::Client.new(@api_key, connection_id)
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
def mcp(connection_id)
|
|
43
|
+
if connection_id.nil? || connection_id.empty?
|
|
44
|
+
raise ArgumentError, 'Connection ID is required to create an MCP instance.'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
BundleUp::MCP.new(@api_key, connection_id)
|
|
48
|
+
end
|
|
41
49
|
end
|
|
42
50
|
end
|
data/lib/bundleup/mcp.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'mcp_client'
|
|
5
|
+
|
|
6
|
+
module BundleUp
|
|
7
|
+
# Transport for a connection's MCP server.
|
|
8
|
+
#
|
|
9
|
+
# +post+ and +delete+ return the raw Faraday response, the way Proxy does.
|
|
10
|
+
# Use +connect+ for a managed session that handles the handshake and
|
|
11
|
+
# response decoding.
|
|
12
|
+
class MCP
|
|
13
|
+
BASE_URL = 'https://mcp.bundleup.io'
|
|
14
|
+
|
|
15
|
+
# Separates the API key from an appended connection ID. Safe to split on:
|
|
16
|
+
# API keys are alphanumeric and connection IDs are cuids.
|
|
17
|
+
CREDENTIAL_SEPARATOR = '.'
|
|
18
|
+
|
|
19
|
+
attr_reader :api_key, :connection_id
|
|
20
|
+
|
|
21
|
+
def initialize(api_key, connection_id)
|
|
22
|
+
@api_key = api_key
|
|
23
|
+
@connection_id = connection_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The URL and headers for an MCP client running in your own process.
|
|
27
|
+
def transport
|
|
28
|
+
{ url: BASE_URL, headers: default_headers }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The URL and a single bearer token carrying both the API key and the
|
|
32
|
+
# connection, for model-hosted MCP clients that cannot set custom headers.
|
|
33
|
+
# Note this hands your API key to the model provider.
|
|
34
|
+
def hosted
|
|
35
|
+
{ url: BASE_URL, token: "#{@api_key}#{CREDENTIAL_SEPARATOR}#{@connection_id}" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Send a JSON-RPC message and return the raw response. Pass
|
|
39
|
+
# +Mcp-Session-Id+ in +headers+ to stay on an existing session.
|
|
40
|
+
def post(body, headers: {})
|
|
41
|
+
payload = body.is_a?(String) ? body : body.to_json
|
|
42
|
+
|
|
43
|
+
connection.post(BASE_URL, payload, default_headers.merge(headers))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# End an MCP session. Pass the session's +Mcp-Session-Id+ in +headers+.
|
|
47
|
+
def delete(headers: {})
|
|
48
|
+
connection.delete(BASE_URL, nil, default_headers.merge(headers))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Open a managed MCP session for this connection.
|
|
52
|
+
def connect
|
|
53
|
+
MCPClient.new(BASE_URL, @api_key, @connection_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def default_headers
|
|
59
|
+
{
|
|
60
|
+
'Authorization' => "Bearer #{@api_key}",
|
|
61
|
+
'Content-Type' => 'application/json',
|
|
62
|
+
'Accept' => 'application/json, text/event-stream',
|
|
63
|
+
'BU-Connection-Id' => @connection_id
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def connection
|
|
68
|
+
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BundleUp
|
|
6
|
+
# A connected MCP session.
|
|
7
|
+
#
|
|
8
|
+
# Tools, resources and prompts are defined by the provider — BundleUp does
|
|
9
|
+
# not rename or normalize them.
|
|
10
|
+
class MCPClient
|
|
11
|
+
PROTOCOL_VERSION = '2025-06-18'
|
|
12
|
+
CLIENT_NAME = 'bundleup-sdk'
|
|
13
|
+
|
|
14
|
+
def initialize(base_url, api_key, connection_id)
|
|
15
|
+
@base_url = base_url
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
@connection_id = connection_id
|
|
18
|
+
@session_id = nil
|
|
19
|
+
@connected = false
|
|
20
|
+
@last_id = 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# List the provider's tools, following pagination to the end.
|
|
24
|
+
def tools
|
|
25
|
+
paginate('tools/list', 'tools')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Call a tool by name, with arguments matching its own input schema.
|
|
29
|
+
def tool(name, args = {})
|
|
30
|
+
raise ArgumentError, 'Tool name is required to call a tool.' if blank?(name)
|
|
31
|
+
|
|
32
|
+
connect
|
|
33
|
+
send_message('tools/call', { name: name, arguments: args })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# List the provider's resources, following pagination to the end.
|
|
37
|
+
def resources
|
|
38
|
+
paginate('resources/list', 'resources')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Read a resource by URI.
|
|
42
|
+
def resource(uri)
|
|
43
|
+
raise ArgumentError, 'Resource URI is required to read a resource.' if blank?(uri)
|
|
44
|
+
|
|
45
|
+
connect
|
|
46
|
+
send_message('resources/read', { uri: uri })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# List the provider's prompts, following pagination to the end.
|
|
50
|
+
def prompts
|
|
51
|
+
paginate('prompts/list', 'prompts')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get a prompt by name.
|
|
55
|
+
def prompt(name, args = {})
|
|
56
|
+
raise ArgumentError, 'Prompt name is required to get a prompt.' if blank?(name)
|
|
57
|
+
|
|
58
|
+
connect
|
|
59
|
+
send_message('prompts/get', { name: name, arguments: args })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Send any other JSON-RPC method on this session.
|
|
63
|
+
def request(method, params = nil)
|
|
64
|
+
raise ArgumentError, 'Method is required to send a request.' if blank?(method)
|
|
65
|
+
|
|
66
|
+
connect
|
|
67
|
+
send_message(method, params)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# End the session and reset local state.
|
|
71
|
+
def close
|
|
72
|
+
delete_session if @session_id
|
|
73
|
+
|
|
74
|
+
@session_id = nil
|
|
75
|
+
@connected = false
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def blank?(value)
|
|
82
|
+
value.nil? || value.to_s.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def default_headers
|
|
86
|
+
headers = {
|
|
87
|
+
'Authorization' => '******',
|
|
88
|
+
'Content-Type' => 'application/json',
|
|
89
|
+
'Accept' => 'application/json, text/event-stream',
|
|
90
|
+
'BU-Connection-Id' => @connection_id
|
|
91
|
+
}
|
|
92
|
+
headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
93
|
+
headers
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def connection
|
|
97
|
+
@connection ||= Faraday.new { |faraday| faraday.adapter Faraday.default_adapter }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def post_payload(payload)
|
|
101
|
+
response = connection.post(@base_url, payload.to_json, default_headers)
|
|
102
|
+
session_id = response.headers['mcp-session-id']
|
|
103
|
+
@session_id = session_id if session_id
|
|
104
|
+
|
|
105
|
+
raise error_for(response) unless response.success?
|
|
106
|
+
|
|
107
|
+
response
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def error_for(response)
|
|
111
|
+
fallback = "MCP request failed with status #{response.status}."
|
|
112
|
+
parsed = JSON.parse(response.body.to_s)
|
|
113
|
+
return RuntimeError.new(fallback) unless parsed.is_a?(Hash) && parsed['message']
|
|
114
|
+
|
|
115
|
+
code = parsed['code']
|
|
116
|
+
RuntimeError.new(code ? "#{parsed['message']} (#{code})" : parsed['message'])
|
|
117
|
+
rescue JSON::ParserError
|
|
118
|
+
RuntimeError.new(fallback)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Run the MCP handshake, once. Deferred until the first call.
|
|
122
|
+
def connect
|
|
123
|
+
return if @connected
|
|
124
|
+
|
|
125
|
+
send_message('initialize', handshake_params)
|
|
126
|
+
post_payload({ jsonrpc: '2.0', method: 'notifications/initialized' })
|
|
127
|
+
@connected = true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def handshake_params
|
|
131
|
+
{
|
|
132
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
133
|
+
capabilities: {},
|
|
134
|
+
clientInfo: { name: CLIENT_NAME, version: BundleUp::VERSION }
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def send_message(method, params = nil)
|
|
139
|
+
@last_id += 1
|
|
140
|
+
payload = { jsonrpc: '2.0', id: @last_id, method: method }
|
|
141
|
+
payload[:params] = params unless params.nil?
|
|
142
|
+
|
|
143
|
+
message = parse(post_payload(payload), @last_id)
|
|
144
|
+
raise "No response received for #{method}." if message.nil?
|
|
145
|
+
raise message['error']['message'].to_s if message['error']
|
|
146
|
+
|
|
147
|
+
message['result'] || {}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Providers may answer a plain request/response over text/event-stream.
|
|
151
|
+
def parse(response, message_id)
|
|
152
|
+
body = response.body.to_s
|
|
153
|
+
return nil if body.empty?
|
|
154
|
+
|
|
155
|
+
content_type = response.headers['content-type'].to_s
|
|
156
|
+
return JSON.parse(body) unless content_type.include?('text/event-stream')
|
|
157
|
+
|
|
158
|
+
parse_stream(body, message_id)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parse_stream(body, message_id)
|
|
162
|
+
body.gsub("\r\n", "\n").split("\n\n").each do |event|
|
|
163
|
+
data = event_data(event)
|
|
164
|
+
next if data.empty?
|
|
165
|
+
|
|
166
|
+
message = JSON.parse(data)
|
|
167
|
+
# Skip server notifications interleaved on the stream.
|
|
168
|
+
return message if message.is_a?(Hash) && message['id'] == message_id
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def event_data(event)
|
|
175
|
+
event.split("\n")
|
|
176
|
+
.select { |line| line.start_with?('data:') }
|
|
177
|
+
.map { |line| line.sub('data:', '').strip }
|
|
178
|
+
.join("\n")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def paginate(method, key)
|
|
182
|
+
connect
|
|
183
|
+
items = []
|
|
184
|
+
cursor = nil
|
|
185
|
+
|
|
186
|
+
loop do
|
|
187
|
+
result = send_message(method, cursor ? { cursor: cursor } : nil)
|
|
188
|
+
items.concat(result[key] || [])
|
|
189
|
+
cursor = result['nextCursor']
|
|
190
|
+
break unless cursor
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
items
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def delete_session
|
|
197
|
+
connection.delete(@base_url, nil, default_headers)
|
|
198
|
+
rescue Faraday::Error
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
data/lib/bundleup/unify/git.rb
CHANGED
|
@@ -17,6 +17,8 @@ module BundleUp
|
|
|
17
17
|
|
|
18
18
|
# Fetches pull requests for a specific repository from the connected Git provider.
|
|
19
19
|
def pulls(repo_name, params = {})
|
|
20
|
+
raise ArgumentError, 'repo_name is required to fetch pulls.' if blank?(repo_name)
|
|
21
|
+
|
|
20
22
|
encoded_repo_name = URI.encode_www_form_component(repo_name)
|
|
21
23
|
|
|
22
24
|
response = connection.get("git/repos/#{encoded_repo_name}/pulls") do |req|
|
|
@@ -30,6 +32,8 @@ module BundleUp
|
|
|
30
32
|
|
|
31
33
|
# Fetches tags for a specific repository from the connected Git provider.
|
|
32
34
|
def tags(repo_name, params = {})
|
|
35
|
+
raise ArgumentError, 'repo_name is required to fetch tags.' if blank?(repo_name)
|
|
36
|
+
|
|
33
37
|
encoded_repo_name = URI.encode_www_form_component(repo_name)
|
|
34
38
|
|
|
35
39
|
response = connection.get("git/repos/#{encoded_repo_name}/tags") do |req|
|
|
@@ -43,6 +47,8 @@ module BundleUp
|
|
|
43
47
|
|
|
44
48
|
# Fetches releases for a specific repository from the connected Git provider.
|
|
45
49
|
def releases(repo_name, params = {})
|
|
50
|
+
raise ArgumentError, 'repo_name is required to fetch releases.' if blank?(repo_name)
|
|
51
|
+
|
|
46
52
|
encoded_repo_name = URI.encode_www_form_component(repo_name)
|
|
47
53
|
|
|
48
54
|
response = connection.get("git/repos/#{encoded_repo_name}/releases") do |req|
|
|
@@ -56,6 +62,8 @@ module BundleUp
|
|
|
56
62
|
|
|
57
63
|
# Fetches branches for a specific repository from the connected Git provider.
|
|
58
64
|
def branches(repo_name, params = {})
|
|
65
|
+
raise ArgumentError, 'repo_name is required to fetch branches.' if blank?(repo_name)
|
|
66
|
+
|
|
59
67
|
encoded_repo_name = URI.encode_www_form_component(repo_name)
|
|
60
68
|
|
|
61
69
|
response = connection.get("git/repos/#{encoded_repo_name}/branches") do |req|
|
|
@@ -66,6 +74,27 @@ module BundleUp
|
|
|
66
74
|
|
|
67
75
|
response.body
|
|
68
76
|
end
|
|
77
|
+
|
|
78
|
+
# Fetches commits for a specific repository from the connected Git provider.
|
|
79
|
+
def commits(repo_name, params = {})
|
|
80
|
+
raise ArgumentError, 'repo_name is required to fetch commits.' if blank?(repo_name)
|
|
81
|
+
|
|
82
|
+
encoded_repo_name = URI.encode_www_form_component(repo_name)
|
|
83
|
+
|
|
84
|
+
response = connection.get("git/repos/#{encoded_repo_name}/commits") do |req|
|
|
85
|
+
req.params = params
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
raise "Failed to fetch git/repos/#{encoded_repo_name}/commits: #{response.status}" unless response.success?
|
|
89
|
+
|
|
90
|
+
response.body
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def blank?(value)
|
|
96
|
+
value.nil? || value.to_s.empty?
|
|
97
|
+
end
|
|
69
98
|
end
|
|
70
99
|
end
|
|
71
100
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
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 tools
|
|
33
|
+
@client.tools
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Call a unified tool with optional arguments.
|
|
37
|
+
def tool(name, args = {})
|
|
38
|
+
raise ArgumentError, 'Tool name is required to call a tool.' if name.nil? || name.to_s.empty?
|
|
39
|
+
|
|
40
|
+
@client.tool(name, args)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/bundleup/unify.rb
CHANGED
|
@@ -40,6 +40,11 @@ module BundleUp
|
|
|
40
40
|
def drive
|
|
41
41
|
@drive ||= BundleUp::Unify::Drive.new(api_key, connection_id)
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
# Access the Unified MCP server for the connection.
|
|
45
|
+
def mcp
|
|
46
|
+
@mcp ||= BundleUp::Unify::MCP.new(api_key, connection_id)
|
|
47
|
+
end
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
50
|
end
|
data/lib/bundleup/version.rb
CHANGED
data/lib/bundleup.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative 'bundleup/version'
|
|
|
6
6
|
require_relative 'bundleup/client'
|
|
7
7
|
require_relative 'bundleup/proxy'
|
|
8
8
|
require_relative 'bundleup/unify'
|
|
9
|
+
require_relative 'bundleup/mcp'
|
|
9
10
|
|
|
10
11
|
# Resources
|
|
11
12
|
require_relative 'bundleup/resources/base'
|
|
@@ -20,6 +21,7 @@ require_relative 'bundleup/unify/git'
|
|
|
20
21
|
require_relative 'bundleup/unify/ticketing'
|
|
21
22
|
require_relative 'bundleup/unify/crm'
|
|
22
23
|
require_relative 'bundleup/unify/drive'
|
|
24
|
+
require_relative 'bundleup/unify/mcp'
|
|
23
25
|
|
|
24
26
|
# Main module for the BundleUp SDK.
|
|
25
27
|
module BundleUp
|
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.4.1
|
|
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-08-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -168,6 +168,8 @@ files:
|
|
|
168
168
|
- README.md
|
|
169
169
|
- lib/bundleup.rb
|
|
170
170
|
- lib/bundleup/client.rb
|
|
171
|
+
- lib/bundleup/mcp.rb
|
|
172
|
+
- lib/bundleup/mcp_client.rb
|
|
171
173
|
- lib/bundleup/proxy.rb
|
|
172
174
|
- lib/bundleup/resources/base.rb
|
|
173
175
|
- lib/bundleup/resources/connection.rb
|
|
@@ -179,6 +181,7 @@ files:
|
|
|
179
181
|
- lib/bundleup/unify/crm.rb
|
|
180
182
|
- lib/bundleup/unify/drive.rb
|
|
181
183
|
- lib/bundleup/unify/git.rb
|
|
184
|
+
- lib/bundleup/unify/mcp.rb
|
|
182
185
|
- lib/bundleup/unify/ticketing.rb
|
|
183
186
|
- lib/bundleup/version.rb
|
|
184
187
|
- sig/bundleup/unify.rbs
|