bundleup-sdk 0.5.0 â 0.7.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 +95 -82
- data/lib/bundleup/auth.rb +14 -1
- data/lib/bundleup/mcp.rb +4 -206
- data/lib/bundleup/unify/helpdesk.rb +40 -0
- data/lib/bundleup/unify/tasks.rb +54 -0
- data/lib/bundleup/unify.rb +10 -9
- data/lib/bundleup/version.rb +1 -1
- data/lib/bundleup.rb +7 -2
- data/sig/bundleup/auth.rbs +18 -0
- data/sig/bundleup/unify/helpdesk.rbs +33 -0
- data/sig/bundleup/unify/{ticketing.rbs â tasks.rbs} +7 -7
- data/sig/bundleup/unify.rbs +2 -1
- metadata +6 -5
- data/lib/bundleup/unify/mcp.rb +0 -44
- data/lib/bundleup/unify/ticketing.rb +0 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3feaef0f54fcef0ff3dba477b926513d0ed0801e2f383a80b0ca13f44218c5a
|
|
4
|
+
data.tar.gz: d44bc81b00d6afa88e0b4d58de1f832a98d81538f00a6169d3f5838e52865d1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95b41fa793e45e3aaa62f01beda29dce8172b5547da1e5959c0c4cf6927de71fca5892f9c2d8d2a87a1fecc3a43af53c7cc2139e1afff4a215f3c5287a057a9d
|
|
7
|
+
data.tar.gz: e597e588f6cd074e365660d54908ef489e456fcec0b9f5238c87ad946be29b007e636fb541439429fd25a6b6486fa89c535eab2d4c76e5dd824d0427248d8b6d
|
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
|
|
@@ -84,7 +84,7 @@ Runnable examples are available in the [`examples/`](./examples) directory:
|
|
|
84
84
|
|
|
85
85
|
- [`examples/basic_usage.rb`](./examples/basic_usage.rb) - Client setup, connections, integrations, and webhooks
|
|
86
86
|
- [`examples/proxy_api.rb`](./examples/proxy_api.rb) - Proxy API GET request with a connection
|
|
87
|
-
- [`examples/unify_api.rb`](./examples/unify_api.rb) - Unify Chat, Git,
|
|
87
|
+
- [`examples/unify_api.rb`](./examples/unify_api.rb) - Unify Chat, Git, Tasks, Helpdesk, Drive, and Calendar endpoint usage
|
|
88
88
|
- [`examples/README.md`](./examples/README.md) - Setup and execution instructions
|
|
89
89
|
|
|
90
90
|
## Quick Start
|
|
@@ -542,6 +542,16 @@ url = client.auth.build_authorization_url(
|
|
|
542
542
|
# Redirect the user to `url`
|
|
543
543
|
```
|
|
544
544
|
|
|
545
|
+
The URL needs no API key, so you can also build it without a client:
|
|
546
|
+
|
|
547
|
+
```ruby
|
|
548
|
+
url = BundleUp.build_authorization_url(
|
|
549
|
+
client_id: 'your-client-id',
|
|
550
|
+
integration_id: 'github',
|
|
551
|
+
redirect_uri: 'https://app.example.com/callback'
|
|
552
|
+
)
|
|
553
|
+
```
|
|
554
|
+
|
|
545
555
|
**Parameters:**
|
|
546
556
|
|
|
547
557
|
- `client_id` (String, required): Your workspace client ID
|
|
@@ -1084,20 +1094,20 @@ provider's default branch is used.
|
|
|
1084
1094
|
}
|
|
1085
1095
|
```
|
|
1086
1096
|
|
|
1087
|
-
####
|
|
1097
|
+
#### Tasks API
|
|
1088
1098
|
|
|
1089
|
-
The
|
|
1099
|
+
The Tasks API provides a unified interface for issue trackers and project management tools like Jira, Linear, Asana, Trello, ClickUp, monday.com, Basecamp, Wrike, and Todoist.
|
|
1090
1100
|
|
|
1091
|
-
##### List
|
|
1101
|
+
##### List Tasks
|
|
1092
1102
|
|
|
1093
1103
|
```ruby
|
|
1094
|
-
result = unify.
|
|
1104
|
+
result = unify.tasks.tasks(
|
|
1095
1105
|
limit: 100,
|
|
1096
1106
|
after: nil,
|
|
1097
1107
|
include_raw: false
|
|
1098
1108
|
)
|
|
1099
1109
|
|
|
1100
|
-
puts "
|
|
1110
|
+
puts "Tasks: #{result['data']}"
|
|
1101
1111
|
```
|
|
1102
1112
|
|
|
1103
1113
|
**Response:**
|
|
@@ -1124,18 +1134,18 @@ puts "Tickets: #{result['data']}"
|
|
|
1124
1134
|
**Filtering and sorting:**
|
|
1125
1135
|
|
|
1126
1136
|
```ruby
|
|
1127
|
-
|
|
1128
|
-
sorted_by_date = result['data'].sort_by { |
|
|
1137
|
+
open_tasks = result['data'].select { |task| task['status'] == 'open' }
|
|
1138
|
+
sorted_by_date = result['data'].sort_by { |task| Time.parse(task['created_at']) }.reverse
|
|
1129
1139
|
```
|
|
1130
1140
|
|
|
1131
|
-
##### Get a
|
|
1141
|
+
##### Get a Task
|
|
1132
1142
|
|
|
1133
|
-
Fetch one
|
|
1143
|
+
Fetch one task by ID. Not supported by Basecamp, whose API only serves a to-do underneath its project.
|
|
1134
1144
|
|
|
1135
1145
|
```ruby
|
|
1136
|
-
result = unify.
|
|
1146
|
+
result = unify.tasks.task('PROJ-123')
|
|
1137
1147
|
|
|
1138
|
-
puts "
|
|
1148
|
+
puts "Task: #{result['data']}"
|
|
1139
1149
|
```
|
|
1140
1150
|
|
|
1141
1151
|
**Response:**
|
|
@@ -1159,7 +1169,7 @@ A single resource carries no pagination, so there is no `metadata` on this respo
|
|
|
1159
1169
|
##### List Projects
|
|
1160
1170
|
|
|
1161
1171
|
```ruby
|
|
1162
|
-
result = unify.
|
|
1172
|
+
result = unify.tasks.projects(
|
|
1163
1173
|
limit: 100,
|
|
1164
1174
|
after: nil,
|
|
1165
1175
|
include_raw: false
|
|
@@ -1191,6 +1201,57 @@ puts "Projects: #{result['data']}"
|
|
|
1191
1201
|
|
|
1192
1202
|
**Note:** Not every platform returns every field. Jira does not expose creation or update timestamps for projects, so `created_at` and `updated_at` are `nil` for Jira connections, and `status` is only set when Jira reports whether the project is archived.
|
|
1193
1203
|
|
|
1204
|
+
#### Helpdesk API
|
|
1205
|
+
|
|
1206
|
+
The Helpdesk API provides a unified interface for customer support tools like Zendesk, ServiceNow, Zoho Desk, Gorgias, and Help Scout.
|
|
1207
|
+
|
|
1208
|
+
##### List Tickets
|
|
1209
|
+
|
|
1210
|
+
```ruby
|
|
1211
|
+
result = unify.helpdesk.tickets(
|
|
1212
|
+
limit: 100,
|
|
1213
|
+
after: nil,
|
|
1214
|
+
include_raw: false
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
puts "Tickets: #{result['data']}"
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
**Response:**
|
|
1221
|
+
|
|
1222
|
+
```ruby
|
|
1223
|
+
{
|
|
1224
|
+
'data' => [
|
|
1225
|
+
{
|
|
1226
|
+
'id' => '4182',
|
|
1227
|
+
'title' => 'Refund for order #1043',
|
|
1228
|
+
'description' => 'Hi, my order arrived damaged...',
|
|
1229
|
+
'status' => 'open',
|
|
1230
|
+
'priority' => 'high',
|
|
1231
|
+
'requester' => { 'id' => '381920', 'name' => 'Dana Ruiz', 'email' => 'dana@example.com' },
|
|
1232
|
+
'url' => 'https://acme.zendesk.com/agent/tickets/4182',
|
|
1233
|
+
'created_at' => '2024-01-15T10:30:00Z',
|
|
1234
|
+
'updated_at' => '2024-01-20T14:22:00Z'
|
|
1235
|
+
}
|
|
1236
|
+
],
|
|
1237
|
+
'metadata' => {
|
|
1238
|
+
'next' => 'cursor_def456'
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
##### Get a Ticket
|
|
1244
|
+
|
|
1245
|
+
Fetch one support ticket by ID. On ServiceNow the ID is the incident's `sys_id`.
|
|
1246
|
+
|
|
1247
|
+
```ruby
|
|
1248
|
+
result = unify.helpdesk.ticket('4182')
|
|
1249
|
+
|
|
1250
|
+
puts "Ticket: #{result['data']}"
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
A single resource carries no pagination, so there is no `metadata` on this response.
|
|
1254
|
+
|
|
1194
1255
|
#### CRM API
|
|
1195
1256
|
|
|
1196
1257
|
The CRM API provides a unified interface for CRM platforms like Attio, HubSpot, PipeDrive, Salesforce and Zoho.
|
|
@@ -1340,47 +1401,14 @@ Reach a provider's own MCP server using a connection's credentials. BundleUp inj
|
|
|
1340
1401
|
|
|
1341
1402
|
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
1403
|
|
|
1343
|
-
|
|
1404
|
+
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
1405
|
|
|
1345
|
-
#### Creating an MCP
|
|
1406
|
+
#### Creating an MCP Instance
|
|
1346
1407
|
|
|
1347
1408
|
```ruby
|
|
1348
1409
|
mcp = client.mcp('conn_123abc')
|
|
1349
1410
|
```
|
|
1350
1411
|
|
|
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
1412
|
#### Model-Hosted MCP
|
|
1385
1413
|
|
|
1386
1414
|
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 +1431,7 @@ response = openai.responses.create(
|
|
|
1403
1431
|
)
|
|
1404
1432
|
```
|
|
1405
1433
|
|
|
1406
|
-
Anthropic's connector takes the same pair as `url` and `authorization_token`.
|
|
1434
|
+
Anthropic's connector takes the same pair as `url` and `authorization_token`.
|
|
1407
1435
|
|
|
1408
1436
|
`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
1437
|
|
|
@@ -1506,45 +1534,29 @@ end
|
|
|
1506
1534
|
|
|
1507
1535
|
Every JSON-RPC message counts toward the rate limit of 100 requests per 60 seconds, per connection â including the `initialize` handshake.
|
|
1508
1536
|
|
|
1509
|
-
####
|
|
1537
|
+
#### Several Connections
|
|
1510
1538
|
|
|
1511
|
-
An agent often needs more than one provider for the same end user.
|
|
1539
|
+
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
1540
|
|
|
1513
1541
|
```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
|
-
}
|
|
1542
|
+
connections = { 'slack' => user.slack_connection, 'linear' => user.linear_connection }
|
|
1519
1543
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
mcp.list_tools.map { |tool| tool.merge('name' => "#{label}__#{tool['name']}") }
|
|
1523
|
-
end
|
|
1544
|
+
tools = connections.map do |label, connection_id|
|
|
1545
|
+
hosted = client.mcp(connection_id).hosted
|
|
1524
1546
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1547
|
+
{
|
|
1548
|
+
type: 'mcp',
|
|
1549
|
+
server_label: label,
|
|
1550
|
+
server_url: hosted[:url],
|
|
1551
|
+
authorization: hosted[:token],
|
|
1552
|
+
require_approval: 'never'
|
|
1553
|
+
}
|
|
1529
1554
|
end
|
|
1530
1555
|
```
|
|
1531
1556
|
|
|
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
|
-
```
|
|
1557
|
+
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
1558
|
|
|
1547
|
-
|
|
1559
|
+
**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
1560
|
|
|
1549
1561
|
## Error Handling
|
|
1550
1562
|
|
|
@@ -1589,7 +1601,7 @@ lib/
|
|
|
1589
1601
|
â âââ client.rb # Main client class
|
|
1590
1602
|
â âââ auth.rb # Auth API (authorization URL + code exchange)
|
|
1591
1603
|
â âââ proxy.rb # Proxy API implementation
|
|
1592
|
-
â âââ mcp.rb # MCP API (transport +
|
|
1604
|
+
â âââ mcp.rb # MCP API (transport + hosted)
|
|
1593
1605
|
â âââ unify.rb # Unify API client wrapper
|
|
1594
1606
|
â âââ version.rb # Gem version
|
|
1595
1607
|
â âââ resources/
|
|
@@ -1601,7 +1613,8 @@ lib/
|
|
|
1601
1613
|
â âââ base.rb # Base Unify class
|
|
1602
1614
|
â âââ chat.rb # Chat Unify API
|
|
1603
1615
|
â âââ git.rb # Git Unify API
|
|
1604
|
-
â âââ
|
|
1616
|
+
â âââ tasks.rb # Tasks Unify API
|
|
1617
|
+
â âââ helpdesk.rb # Helpdesk Unify API
|
|
1605
1618
|
â âââ crm.rb # CRM Unify API
|
|
1606
1619
|
â âââ drive.rb # Drive Unify API
|
|
1607
1620
|
â âââ calendar.rb # Calendar Unify API
|
data/lib/bundleup/auth.rb
CHANGED
|
@@ -14,7 +14,10 @@ module BundleUp
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Builds the hosted authorization URL to send the end user to.
|
|
17
|
-
|
|
17
|
+
#
|
|
18
|
+
# Needs no API key: /authorize only takes the public client and
|
|
19
|
+
# integration ids, so this is safe to call where the key must not live.
|
|
20
|
+
def self.build_authorization_url(client_id:, integration_id:, redirect_uri:, external_id: nil, state: nil)
|
|
18
21
|
raise ArgumentError, 'client_id is required' if blank?(client_id)
|
|
19
22
|
raise ArgumentError, 'integration_id is required' if blank?(integration_id)
|
|
20
23
|
raise ArgumentError, 'redirect_uri is required' if blank?(redirect_uri)
|
|
@@ -30,6 +33,16 @@ module BundleUp
|
|
|
30
33
|
"#{BASE_URL}/authorize?#{URI.encode_www_form(params)}"
|
|
31
34
|
end
|
|
32
35
|
|
|
36
|
+
def self.blank?(value)
|
|
37
|
+
value.nil? || value.to_s.empty?
|
|
38
|
+
end
|
|
39
|
+
private_class_method :blank?
|
|
40
|
+
|
|
41
|
+
# Builds the hosted authorization URL to send the end user to.
|
|
42
|
+
def build_authorization_url(**params)
|
|
43
|
+
self.class.build_authorization_url(**params)
|
|
44
|
+
end
|
|
45
|
+
|
|
33
46
|
# Exchanges the one-time code from the redirect for the connection.
|
|
34
47
|
# Call this server-side; the code expires after 5 minutes and works once.
|
|
35
48
|
# Returns { 'connection_id', 'external_id', 'integration_id' }.
|
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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BundleUp
|
|
4
|
+
module Unify
|
|
5
|
+
# Client for helpdesk Unify endpoints (customer support tools).
|
|
6
|
+
class Helpdesk < Base
|
|
7
|
+
# Fetches support tickets from the connected helpdesk.
|
|
8
|
+
def tickets(params = {})
|
|
9
|
+
response = connection.get('helpdesk/tickets') do |req|
|
|
10
|
+
req.params = params
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
raise "Failed to fetch helpdesk/tickets: #{response.status}" unless response.success?
|
|
14
|
+
|
|
15
|
+
response.body
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Fetches a single support ticket by ID (a sys_id on ServiceNow).
|
|
19
|
+
def ticket(ticket_id, params = {})
|
|
20
|
+
raise ArgumentError, 'ticket_id is required to fetch a ticket.' if blank?(ticket_id)
|
|
21
|
+
|
|
22
|
+
encoded_id = URI.encode_www_form_component(ticket_id)
|
|
23
|
+
|
|
24
|
+
response = connection.get("helpdesk/tickets/#{encoded_id}") do |req|
|
|
25
|
+
req.params = params
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
raise "Failed to fetch helpdesk/tickets/#{encoded_id}: #{response.status}" unless response.success?
|
|
29
|
+
|
|
30
|
+
response.body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def blank?(value)
|
|
36
|
+
value.nil? || value.to_s.empty?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BundleUp
|
|
4
|
+
module Unify
|
|
5
|
+
# Client for tasks Unify endpoints (issue trackers and project tools).
|
|
6
|
+
class Tasks < Base
|
|
7
|
+
# Fetches tasks from the connected issue tracker or project tool.
|
|
8
|
+
def tasks(params = {})
|
|
9
|
+
response = connection.get('tasks/tasks') do |req|
|
|
10
|
+
req.params = params
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
raise "Failed to fetch tasks/tasks: #{response.status}" unless response.success?
|
|
14
|
+
|
|
15
|
+
response.body
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Fetches a single task by ID from the connected issue tracker or project tool.
|
|
19
|
+
#
|
|
20
|
+
# Not supported by Basecamp, whose API only serves a to-do underneath its
|
|
21
|
+
# project â an id on its own cannot address one.
|
|
22
|
+
def task(task_id, params = {})
|
|
23
|
+
raise ArgumentError, 'task_id is required to fetch a task.' if blank?(task_id)
|
|
24
|
+
|
|
25
|
+
encoded_id = URI.encode_www_form_component(task_id)
|
|
26
|
+
|
|
27
|
+
response = connection.get("tasks/tasks/#{encoded_id}") do |req|
|
|
28
|
+
req.params = params
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise "Failed to fetch tasks/tasks/#{encoded_id}: #{response.status}" unless response.success?
|
|
32
|
+
|
|
33
|
+
response.body
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Fetches projects from the connected issue tracker or project tool.
|
|
37
|
+
def projects(params = {})
|
|
38
|
+
response = connection.get('tasks/projects') do |req|
|
|
39
|
+
req.params = params
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
raise "Failed to fetch tasks/projects: #{response.status}" unless response.success?
|
|
43
|
+
|
|
44
|
+
response.body
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def blank?(value)
|
|
50
|
+
value.nil? || value.to_s.empty?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/bundleup/unify.rb
CHANGED
|
@@ -7,7 +7,8 @@ module BundleUp
|
|
|
7
7
|
attr_reader :api_key, :connection_id
|
|
8
8
|
|
|
9
9
|
def initialize(api_key, connection_id)
|
|
10
|
-
@
|
|
10
|
+
@tasks = BundleUp::Unify::Tasks.new(api_key, connection_id)
|
|
11
|
+
@helpdesk = BundleUp::Unify::Helpdesk.new(api_key, connection_id)
|
|
11
12
|
@chat = BundleUp::Unify::Chat.new(api_key, connection_id)
|
|
12
13
|
@git = BundleUp::Unify::Git.new(api_key, connection_id)
|
|
13
14
|
@crm = BundleUp::Unify::CRM.new(api_key, connection_id)
|
|
@@ -32,9 +33,14 @@ module BundleUp
|
|
|
32
33
|
@crm ||= BundleUp::Unify::CRM.new(api_key, connection_id)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
# Access the
|
|
36
|
-
def
|
|
37
|
-
@
|
|
36
|
+
# Access the Tasks API (issue trackers and project tools) for the connection.
|
|
37
|
+
def tasks
|
|
38
|
+
@tasks ||= BundleUp::Unify::Tasks.new(api_key, connection_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Access the Helpdesk API (customer support tools) for the connection.
|
|
42
|
+
def helpdesk
|
|
43
|
+
@helpdesk ||= BundleUp::Unify::Helpdesk.new(api_key, connection_id)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# Access the Drive API for the connection.
|
|
@@ -54,11 +60,6 @@ module BundleUp
|
|
|
54
60
|
def me(params = {})
|
|
55
61
|
(@me ||= BundleUp::Unify::Me.new(api_key, connection_id)).get(params)
|
|
56
62
|
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
63
|
end
|
|
63
64
|
end
|
|
64
65
|
end
|
data/lib/bundleup/version.rb
CHANGED
data/lib/bundleup.rb
CHANGED
|
@@ -19,13 +19,18 @@ require_relative 'bundleup/resources/webhook'
|
|
|
19
19
|
require_relative 'bundleup/unify/base'
|
|
20
20
|
require_relative 'bundleup/unify/chat'
|
|
21
21
|
require_relative 'bundleup/unify/git'
|
|
22
|
-
require_relative 'bundleup/unify/
|
|
22
|
+
require_relative 'bundleup/unify/tasks'
|
|
23
|
+
require_relative 'bundleup/unify/helpdesk'
|
|
23
24
|
require_relative 'bundleup/unify/crm'
|
|
24
25
|
require_relative 'bundleup/unify/drive'
|
|
25
26
|
require_relative 'bundleup/unify/calendar'
|
|
26
|
-
require_relative 'bundleup/unify/mcp'
|
|
27
27
|
require_relative 'bundleup/unify/me'
|
|
28
28
|
|
|
29
29
|
# Main module for the BundleUp SDK.
|
|
30
30
|
module BundleUp
|
|
31
|
+
# Builds the hosted authorization URL without an API key.
|
|
32
|
+
# See BundleUp::Auth.build_authorization_url.
|
|
33
|
+
def self.build_authorization_url(**params)
|
|
34
|
+
Auth.build_authorization_url(**params)
|
|
35
|
+
end
|
|
31
36
|
end
|
data/sig/bundleup/auth.rbs
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
module BundleUp
|
|
2
|
+
def self.build_authorization_url: (
|
|
3
|
+
client_id: String,
|
|
4
|
+
integration_id: String,
|
|
5
|
+
redirect_uri: String,
|
|
6
|
+
?external_id: String?,
|
|
7
|
+
?state: String?
|
|
8
|
+
) -> String
|
|
9
|
+
|
|
2
10
|
class Auth
|
|
3
11
|
BASE_URL: String
|
|
4
12
|
|
|
@@ -12,6 +20,16 @@ module BundleUp
|
|
|
12
20
|
|
|
13
21
|
def initialize: (String api_key) -> void
|
|
14
22
|
|
|
23
|
+
def self.blank?: (untyped value) -> bool
|
|
24
|
+
|
|
25
|
+
def self.build_authorization_url: (
|
|
26
|
+
client_id: String,
|
|
27
|
+
integration_id: String,
|
|
28
|
+
redirect_uri: String,
|
|
29
|
+
?external_id: String?,
|
|
30
|
+
?state: String?
|
|
31
|
+
) -> String
|
|
32
|
+
|
|
15
33
|
def build_authorization_url: (
|
|
16
34
|
client_id: String,
|
|
17
35
|
integration_id: String,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module BundleUp
|
|
2
|
+
module Unify
|
|
3
|
+
class Helpdesk < Base
|
|
4
|
+
type requester = {
|
|
5
|
+
id: String?,
|
|
6
|
+
name: String?,
|
|
7
|
+
email: String?
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ticket = {
|
|
11
|
+
id: String,
|
|
12
|
+
title: String?,
|
|
13
|
+
description: String?,
|
|
14
|
+
status: String?,
|
|
15
|
+
priority: String?,
|
|
16
|
+
requester: requester?,
|
|
17
|
+
url: String?,
|
|
18
|
+
created_at: String?,
|
|
19
|
+
updated_at: String?
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type metadata = { next: String? }
|
|
23
|
+
|
|
24
|
+
def tickets: (?Hash[Symbol, untyped] params) -> { data: Array[ticket], metadata: metadata }
|
|
25
|
+
|
|
26
|
+
def ticket: (String ticket_id, ?Hash[Symbol, untyped] params) -> { data: ticket }
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def blank?: (untyped) -> bool
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module BundleUp
|
|
2
2
|
module Unify
|
|
3
|
-
class
|
|
4
|
-
type
|
|
3
|
+
class Tasks < Base
|
|
4
|
+
type task_item = {
|
|
5
5
|
id: String,
|
|
6
6
|
title: String,
|
|
7
7
|
status: String,
|
|
@@ -11,7 +11,7 @@ module BundleUp
|
|
|
11
11
|
updated_at: String
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type
|
|
14
|
+
type task = {
|
|
15
15
|
id: String,
|
|
16
16
|
title: String,
|
|
17
17
|
status: String?,
|
|
@@ -21,7 +21,7 @@ module BundleUp
|
|
|
21
21
|
updated_at: String?
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
type
|
|
24
|
+
type project = {
|
|
25
25
|
id: String,
|
|
26
26
|
name: String,
|
|
27
27
|
status: String?,
|
|
@@ -33,11 +33,11 @@ module BundleUp
|
|
|
33
33
|
|
|
34
34
|
type metadata = { next: String? }
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def tasks: (?Hash[Symbol, untyped] params) -> { data: Array[task_item], metadata: metadata }
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def task: (String task_id, ?Hash[Symbol, untyped] params) -> { data: task }
|
|
39
39
|
|
|
40
|
-
def projects: (?Hash[Symbol, untyped] params) -> { data: Array[
|
|
40
|
+
def projects: (?Hash[Symbol, untyped] params) -> { data: Array[project], metadata: metadata }
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
data/sig/bundleup/unify.rbs
CHANGED
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.7.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-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -182,9 +182,9 @@ 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/
|
|
185
|
+
- lib/bundleup/unify/helpdesk.rb
|
|
186
186
|
- lib/bundleup/unify/me.rb
|
|
187
|
-
- lib/bundleup/unify/
|
|
187
|
+
- lib/bundleup/unify/tasks.rb
|
|
188
188
|
- lib/bundleup/version.rb
|
|
189
189
|
- sig/bundleup/auth.rbs
|
|
190
190
|
- sig/bundleup/unify.rbs
|
|
@@ -194,8 +194,9 @@ files:
|
|
|
194
194
|
- sig/bundleup/unify/crm.rbs
|
|
195
195
|
- sig/bundleup/unify/drive.rbs
|
|
196
196
|
- sig/bundleup/unify/git.rbs
|
|
197
|
+
- sig/bundleup/unify/helpdesk.rbs
|
|
197
198
|
- sig/bundleup/unify/me.rbs
|
|
198
|
-
- sig/bundleup/unify/
|
|
199
|
+
- sig/bundleup/unify/tasks.rbs
|
|
199
200
|
homepage: https://github.com/bundleup/bundleup-sdk-ruby
|
|
200
201
|
licenses:
|
|
201
202
|
- MIT
|
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
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BundleUp
|
|
4
|
-
module Unify
|
|
5
|
-
# Client for ticketing Unify endpoints.
|
|
6
|
-
class Ticketing < Base
|
|
7
|
-
# Fetches tickets from the connected ticketing tool.
|
|
8
|
-
def tickets(params = {})
|
|
9
|
-
response = connection.get('ticketing/tickets') do |req|
|
|
10
|
-
req.params = params
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
raise "Failed to fetch ticketing/tickets: #{response.status}" unless response.success?
|
|
14
|
-
|
|
15
|
-
response.body
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Fetches a single ticket by ID from the connected ticketing tool.
|
|
19
|
-
#
|
|
20
|
-
# Not supported by Basecamp, whose API only serves a to-do underneath its
|
|
21
|
-
# project â an id on its own cannot address one.
|
|
22
|
-
def ticket(ticket_id, params = {})
|
|
23
|
-
raise ArgumentError, 'ticket_id is required to fetch a ticket.' if blank?(ticket_id)
|
|
24
|
-
|
|
25
|
-
encoded_id = URI.encode_www_form_component(ticket_id)
|
|
26
|
-
|
|
27
|
-
response = connection.get("ticketing/tickets/#{encoded_id}") do |req|
|
|
28
|
-
req.params = params
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
raise "Failed to fetch ticketing/tickets/#{encoded_id}: #{response.status}" unless response.success?
|
|
32
|
-
|
|
33
|
-
response.body
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Fetches projects from the connected ticketing tool.
|
|
37
|
-
def projects(params = {})
|
|
38
|
-
response = connection.get('ticketing/projects') do |req|
|
|
39
|
-
req.params = params
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
raise "Failed to fetch ticketing/projects: #{response.status}" unless response.success?
|
|
43
|
-
|
|
44
|
-
response.body
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def blank?(value)
|
|
50
|
-
value.nil? || value.to_s.empty?
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|