rootr-cli 0.17.3 → 0.19.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.
- package/README.ko.md +107 -3
- package/README.md +86 -4
- package/bin/rootr.js +688 -13
- package/lib/client.js +250 -26
- package/lib/credentials.js +20 -2
- package/lib/mcp-tools/ask.js +6 -1
- package/lib/mcp-tools/attachments.js +31 -11
- package/lib/mcp-tools/booking.js +403 -0
- package/lib/mcp-tools/databases.js +490 -20
- package/lib/mcp-tools/documents.js +183 -16
- package/lib/mcp-tools/forms.js +63 -1
- package/lib/mcp-tools/logs.js +1 -1
- package/lib/mcp-tools/misc.js +9 -3
- package/lib/mcp-tools/pages.js +31 -7
- package/lib/mcp-tools/presentations.js +36 -12
- package/lib/mcp-tools/public.js +4 -1
- package/lib/mcp-tools/recordings.js +39 -15
- package/lib/mcp-tools/shared.js +28 -0
- package/lib/mcp-tools/workspaces.js +155 -77
- package/lib/mcp.js +3 -0
- package/lib/resolve.js +43 -1
- package/package.json +1 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BOOKING — 공개한 표를 **예약 페이지**로 쓰는 도구들(창업자 지시 2026-09-23).
|
|
7
|
+
*
|
|
8
|
+
* 화면에서 하던 것을 에이전트도 그대로 할 수 있어야 한다는 요구다. 그래서 여기에는
|
|
9
|
+
* 켜기·규칙·확정 방식·물어볼 칸·구글 캘린더 고르기, 그리고 **방문자가 보는 빈 시간**까지
|
|
10
|
+
* 다 있다 — 마지막 것이 없으면 에이전트는 자기가 건 규칙이 실제로 어떻게 보이는지 모른 채
|
|
11
|
+
* "설정했습니다"라고만 말하게 된다.
|
|
12
|
+
*
|
|
13
|
+
* 구글 연결(동의)은 여기에 없다. 구글이 **브라우저에서 사람 손으로 한 번** 허용받기를
|
|
14
|
+
* 요구하기 때문이다. 대신 `rootr_connect_google_calendar` 가 그 주소를 만들어 주고,
|
|
15
|
+
* 허용한 뒤로는 목록 보기·고르기가 전부 도구로 된다.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const WEEKDAY_HINT =
|
|
19
|
+
'0=Sunday … 6=Saturday. Weekdays only is [1,2,3,4,5].';
|
|
20
|
+
|
|
21
|
+
const ASK_FIELD = z.object({
|
|
22
|
+
propId: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe('Column id, or the column NAME as shown in the table (names are resolved for you).'),
|
|
25
|
+
required: z.boolean().optional().describe('Must the visitor fill this in? Default false.'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 열어 둔 시간 중 **못 잡게 막는 구간**. 하루를 통째로 쉬는 것은 blockedDates 로 두고,
|
|
30
|
+
* 여기는 "매일 점심", "매주 월요일 오전", "10월 5일 오후만" 같은 시간 구간을 다룬다.
|
|
31
|
+
*/
|
|
32
|
+
const BLOCK = z.object({
|
|
33
|
+
kind: z
|
|
34
|
+
.enum(['date', 'daily', 'weekly'])
|
|
35
|
+
.describe('date = that one day · daily = every day · weekly = every week on that weekday.'),
|
|
36
|
+
date: z.string().optional().describe('kind="date": the day, "YYYY-MM-DD".'),
|
|
37
|
+
weekday: z.number().optional().describe(`kind="weekly": which day. ${WEEKDAY_HINT}`),
|
|
38
|
+
start: z.string().optional().describe('"HH:mm". Leave out to block from the start of the day.'),
|
|
39
|
+
end: z.string().optional().describe('"HH:mm". Leave out to block until the end of the day.'),
|
|
40
|
+
label: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Your own note (e.g. "lunch"). Only the owner sees it — visitors never do.'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const GOOGLE_SCHEMA = z.object({
|
|
47
|
+
connectionId: z
|
|
48
|
+
.string()
|
|
49
|
+
.describe('Connection id from rootr_list_google_calendars. The connection belongs to ONE person.'),
|
|
50
|
+
busyCalendarIds: z
|
|
51
|
+
.array(z.string())
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Calendars whose busy times block bookings. Only free/busy is read — never event titles.'),
|
|
54
|
+
writeCalendarId: z
|
|
55
|
+
.string()
|
|
56
|
+
.nullable()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Calendar that receives confirmed bookings. null = do not write anything to Google.'),
|
|
59
|
+
meet: z
|
|
60
|
+
.boolean()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
'Also create a Google Meet room on each booking. Needs writeCalendarId. No extra permission is needed. ' +
|
|
64
|
+
'Rootr puts the link in a "회의 링크" column (created for you) and in both confirmation emails.',
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export function registerBookingTools(server) {
|
|
69
|
+
server.registerTool(
|
|
70
|
+
'rootr_get_booking',
|
|
71
|
+
{
|
|
72
|
+
title: 'Read a table\'s visitor-booking settings',
|
|
73
|
+
description:
|
|
74
|
+
'Read the visitor booking (scheduling) setup of a Rootr (루터) DATABASE: whether it is on, the rules ' +
|
|
75
|
+
'(days, hours, slot length, gap, notice, how far ahead, per-day cap, days off, blocked times, timezone), whether bookings ' +
|
|
76
|
+
'are confirmed instantly or after approval, who gets notified by email, whether a Google Meet room is created, ' +
|
|
77
|
+
'which columns visitors are asked for, and which Google Calendar ' +
|
|
78
|
+
'account/calendars are wired up. Also reports whether the table is published (booking only works on a ' +
|
|
79
|
+
'published table) and whether any booking failed to reach Google. Use this before changing anything.',
|
|
80
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
81
|
+
inputSchema: {
|
|
82
|
+
target: z.string().describe(`DATABASE node id${NODE_TARGET_HINT}`),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
async ({ target }) => {
|
|
86
|
+
try {
|
|
87
|
+
const client = makeClient();
|
|
88
|
+
const id = await resolveNodeId(client, target);
|
|
89
|
+
return jsonResult(await client.getBooking(id));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return errorResult(err);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
server.registerTool(
|
|
97
|
+
'rootr_set_booking',
|
|
98
|
+
{
|
|
99
|
+
title: 'Turn on / configure visitor booking on a table',
|
|
100
|
+
description:
|
|
101
|
+
'Turn a published Rootr (루터) DATABASE into a booking page and set its rules. Visitors open the public ' +
|
|
102
|
+
'link, pick a date, pick an open time, and their booking lands as ONE ROW in the table. ' +
|
|
103
|
+
'ONLY the fields you pass change; everything else is left alone. ' +
|
|
104
|
+
'The table must be published first (rootr_publish_node) — otherwise this returns an error saying so. ' +
|
|
105
|
+
'Columns you do not have are CREATED for you: a date column, an email column, and (for approval mode) a ' +
|
|
106
|
+
'status column with 승인 대기/확정/거절. ' +
|
|
107
|
+
'Visitors never see other people\'s bookings: rows are stripped from the public payload and cloning is ' +
|
|
108
|
+
'blocked while booking is on. Two people clicking the same slot: only one gets it. ' +
|
|
109
|
+
'Google Calendar is OPTIONAL — pass `google` only after rootr_list_google_calendars shows a connection ' +
|
|
110
|
+
'(picking a connection requires a human account, not a bare API key).',
|
|
111
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
112
|
+
inputSchema: {
|
|
113
|
+
target: z.string().describe(`DATABASE node id${NODE_TARGET_HINT}`),
|
|
114
|
+
enabled: z.boolean().optional().describe('Turn visitor booking on or off.'),
|
|
115
|
+
weekdays: z.array(z.number()).optional().describe(`Days that accept bookings. ${WEEKDAY_HINT}`),
|
|
116
|
+
startTime: z.string().optional().describe('First bookable time, "HH:mm" (e.g. "10:00"). Owner timezone.'),
|
|
117
|
+
endTime: z.string().optional().describe('Last bookable time ends by this, "HH:mm" (e.g. "18:00").'),
|
|
118
|
+
durationMin: z
|
|
119
|
+
.number()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('Length of one booking in minutes: 15, 20, 30, 45, 60, 90, 120, 180 or 240.'),
|
|
122
|
+
bufferMin: z.number().optional().describe('Gap kept before AND after each booking, in minutes.'),
|
|
123
|
+
minNoticeHours: z.number().optional().describe('No bookings closer than this many hours from now.'),
|
|
124
|
+
maxDaysAhead: z.number().optional().describe('How far ahead visitors may book, in days.'),
|
|
125
|
+
maxPerDay: z.number().optional().describe('Max bookings per day. 0 = unlimited.'),
|
|
126
|
+
blockedDates: z
|
|
127
|
+
.array(z.string())
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('Days off, "YYYY-MM-DD". A date-only entry already in the table also blocks that day.'),
|
|
130
|
+
blockedTimes: z
|
|
131
|
+
.array(BLOCK)
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(
|
|
134
|
+
'Times that stay closed inside the open hours. Replaces the whole list — send every rule you want kept. ' +
|
|
135
|
+
'Leave out both times to block the whole day: {"kind":"weekly","weekday":5} closes every Friday. ' +
|
|
136
|
+
'Examples: one afternoon {"kind":"date","date":"2026-10-05","start":"13:00","end":"15:00"}; ' +
|
|
137
|
+
'lunch every day {"kind":"daily","start":"12:00","end":"13:00"}; ' +
|
|
138
|
+
'Monday mornings {"kind":"weekly","weekday":1,"start":"09:00","end":"11:00"}.',
|
|
139
|
+
),
|
|
140
|
+
timezone: z.string().optional().describe('IANA timezone the rules and the visitor page use (e.g. "Asia/Seoul").'),
|
|
141
|
+
approval: z
|
|
142
|
+
.enum(['instant', 'manual'])
|
|
143
|
+
.optional()
|
|
144
|
+
.describe(
|
|
145
|
+
'instant = confirmed the moment the visitor books (and written to Google right away). ' +
|
|
146
|
+
'manual = arrives as "승인 대기"; it still HOLDS the slot, and you confirm or decline in the table.',
|
|
147
|
+
),
|
|
148
|
+
notice: z.string().optional().describe('Short note shown at the top of the visitor page.'),
|
|
149
|
+
askFields: z
|
|
150
|
+
.array(ASK_FIELD)
|
|
151
|
+
.optional()
|
|
152
|
+
.describe(
|
|
153
|
+
'Extra columns to ask the visitor for. Name and email are ALWAYS asked, so do not list them. ' +
|
|
154
|
+
'Only text/number/select/multi_select/checkbox/url/email/phone columns can be asked.',
|
|
155
|
+
),
|
|
156
|
+
google: GOOGLE_SCHEMA.optional().describe('Google Calendar wiring. Omit to leave it untouched.'),
|
|
157
|
+
notifyVisitor: z
|
|
158
|
+
.boolean()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe('Email the visitor when their booking is confirmed, still pending, or declined. On by default.'),
|
|
161
|
+
allowVisitorCancel: z
|
|
162
|
+
.boolean()
|
|
163
|
+
.optional()
|
|
164
|
+
.describe(
|
|
165
|
+
'Put a cancel link in the visitor\'s email so they can cancel without an account. ' +
|
|
166
|
+
'The link is a secret only that one booking\'s email carries. On by default; turning it off also ' +
|
|
167
|
+
'refuses links already sent.',
|
|
168
|
+
),
|
|
169
|
+
types: z
|
|
170
|
+
.array(
|
|
171
|
+
z.object({
|
|
172
|
+
id: z.string().optional().describe('Short label used in the ?type= link. Derived from the name when omitted.'),
|
|
173
|
+
name: z.string(),
|
|
174
|
+
durationMin: z.number().int(),
|
|
175
|
+
description: z.string().optional(),
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
.optional()
|
|
179
|
+
.describe(
|
|
180
|
+
'Several kinds of appointment at ONE address, e.g. a 15-minute call and a 60-minute demo. ' +
|
|
181
|
+
'Replaces the whole list; send [] for a single kind using durationMin. The visitor picks one before ' +
|
|
182
|
+
'the calendar appears, and ?type=<id> links straight to one. All kinds share the same busy time, ' +
|
|
183
|
+
'which is why this beats making one table per kind. A column records which kind was booked.',
|
|
184
|
+
),
|
|
185
|
+
hosts: z
|
|
186
|
+
.array(
|
|
187
|
+
z.object({
|
|
188
|
+
id: z.string().optional(),
|
|
189
|
+
name: z.string(),
|
|
190
|
+
email: z.string(),
|
|
191
|
+
google: GOOGLE_SCHEMA.optional().nullable().describe("That host's own Google Calendar wiring, so their busy time blocks their slots."),
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
.optional()
|
|
195
|
+
.describe(
|
|
196
|
+
'The people who take these appointments. Replaces the whole list; send [] to drop the idea of hosts. ' +
|
|
197
|
+
'A column records who got each booking, and each host is emailed their own bookings.',
|
|
198
|
+
),
|
|
199
|
+
assign: z
|
|
200
|
+
.enum(['none', 'roundRobin', 'collective'])
|
|
201
|
+
.optional()
|
|
202
|
+
.describe(
|
|
203
|
+
'How the hosts are used. roundRobin: ONE host takes it — a slot is open when ANY host is free, and the ' +
|
|
204
|
+
'booking goes to whoever has taken the fewest so far. collective: EVERYONE joins — a slot is open only ' +
|
|
205
|
+
'when ALL hosts are free. none (default): no hosts. Ignored unless `hosts` is non-empty.',
|
|
206
|
+
),
|
|
207
|
+
allowGuests: z
|
|
208
|
+
.boolean()
|
|
209
|
+
.optional()
|
|
210
|
+
.describe('Let the visitor add other email addresses. Guests get the same emails and the same calendar invite.'),
|
|
211
|
+
maxGuests: z.number().int().optional().describe('How many guests a visitor may add (default 5). 0 turns it off.'),
|
|
212
|
+
redirectUrl: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe(
|
|
216
|
+
'Where to send the visitor after they book — your own thank-you page. http/https only; anything else is ' +
|
|
217
|
+
'dropped. Empty keeps them on the Rootr confirmation screen.',
|
|
218
|
+
),
|
|
219
|
+
allowVisitorReschedule: z
|
|
220
|
+
.boolean()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe(
|
|
223
|
+
'Let the visitor move their booking to another time from the same emailed link, without an account. ' +
|
|
224
|
+
'One locked check picks the new slot, so there is no gap where someone else can take it. On by default.',
|
|
225
|
+
),
|
|
226
|
+
reminders: z
|
|
227
|
+
.array(z.number().int())
|
|
228
|
+
.optional()
|
|
229
|
+
.describe(
|
|
230
|
+
'Minutes before the meeting to email both sides a reminder, e.g. [1440, 60] for a day and an hour before. ' +
|
|
231
|
+
'Up to 4 values; [] stops reminders. Defaults to [1440]. Only confirmed bookings are reminded, and a ' +
|
|
232
|
+
'reminder whose moment already passed when the booking was made is skipped instead of firing at once.',
|
|
233
|
+
),
|
|
234
|
+
notifyEmails: z
|
|
235
|
+
.array(z.string())
|
|
236
|
+
.optional()
|
|
237
|
+
.describe(
|
|
238
|
+
'Who on the owner side gets an email for each new booking. Replaces the whole list; send [] to stop. ' +
|
|
239
|
+
'Defaults to the account that first set booking up.',
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
async ({ target, askFields, ...rest }) => {
|
|
244
|
+
try {
|
|
245
|
+
const client = makeClient();
|
|
246
|
+
const id = await resolveNodeId(client, target);
|
|
247
|
+
const body = { ...rest };
|
|
248
|
+
if (askFields) {
|
|
249
|
+
// 칸 이름으로 골라도 받는다 — 사람도 에이전트도 칸 id 를 외우고 있지 않다.
|
|
250
|
+
const db = await client.getDatabase(id);
|
|
251
|
+
const props = db.properties || [];
|
|
252
|
+
const ids = new Set(props.map((p) => p.id));
|
|
253
|
+
const byName = new Map(props.map((p) => [p.name, p.id]));
|
|
254
|
+
body.askFields = askFields.map((a) => {
|
|
255
|
+
if (ids.has(a.propId)) return a;
|
|
256
|
+
const found = byName.get(a.propId);
|
|
257
|
+
if (!found) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`No column "${a.propId}" in this table. Columns: ${props.map((p) => p.name).join(', ')}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return { ...a, propId: found };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return jsonResult(await client.setBooking(id, body));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
return errorResult(err);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
server.registerTool(
|
|
273
|
+
'rootr_booking_slots',
|
|
274
|
+
{
|
|
275
|
+
title: 'See the open times visitors see',
|
|
276
|
+
description:
|
|
277
|
+
'List the times a visitor could actually book on a Rootr (루터) booking page, day by day. This is the ' +
|
|
278
|
+
'SAME calculation the public page and the server use, so it is the way to check that the rules you set ' +
|
|
279
|
+
'do what you meant — including which days are closed and which times the owner\'s Google Calendar blocks. ' +
|
|
280
|
+
'Days with nothing open are omitted entirely. If Google Calendar is wired up but unreachable, this errors ' +
|
|
281
|
+
'rather than showing times that might already be taken.',
|
|
282
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
283
|
+
inputSchema: {
|
|
284
|
+
target: z.string().describe(`DATABASE node id${NODE_TARGET_HINT}`),
|
|
285
|
+
from: z.string().optional().describe('First day to look at, "YYYY-MM-DD". Defaults to today.'),
|
|
286
|
+
to: z.string().optional().describe('Last day, "YYYY-MM-DD". At most 62 days after `from`.'),
|
|
287
|
+
type: z
|
|
288
|
+
.string()
|
|
289
|
+
.optional()
|
|
290
|
+
.describe(
|
|
291
|
+
'Which kind of appointment to look at, when the page takes several (the `id` from `types`). Each kind ' +
|
|
292
|
+
'has its own length, so the open times differ. Omitted or unknown falls back to the first kind.',
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
async ({ target, from, to, type }) => {
|
|
297
|
+
try {
|
|
298
|
+
const client = makeClient();
|
|
299
|
+
const id = await resolveNodeId(client, target);
|
|
300
|
+
const status = await client.nodePublicStatus(id);
|
|
301
|
+
if (!status.slug) {
|
|
302
|
+
throw new Error('This table is not published yet — publish it first (rootr_publish_node).');
|
|
303
|
+
}
|
|
304
|
+
const today = new Date(Date.now() + 9 * 3600e3).toISOString().slice(0, 10);
|
|
305
|
+
const start = from || today;
|
|
306
|
+
const end = to || new Date(Date.parse(`${start}T00:00:00Z`) + 30 * 864e5).toISOString().slice(0, 10);
|
|
307
|
+
return jsonResult(await client.bookingSlots(status.slug, { from: start, to: end, type }));
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return errorResult(err);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
server.registerTool(
|
|
315
|
+
'rootr_list_google_calendars',
|
|
316
|
+
{
|
|
317
|
+
title: 'List connected Google accounts and their calendars',
|
|
318
|
+
description:
|
|
319
|
+
'List the Google accounts this PERSON has connected to Rootr (루터) for booking, and — when you pass a ' +
|
|
320
|
+
'connectionId — the calendars on that account with whether each one can be written to. ' +
|
|
321
|
+
'Use the ids from here in rootr_set_booking `google`. ' +
|
|
322
|
+
'A Google connection belongs to one person, so this needs a human account: a bare API key (legacy or the ' +
|
|
323
|
+
'hosted MCP connector) gets a clear error instead of an empty list. If `available` is false, the server ' +
|
|
324
|
+
'itself has no Google setup yet and booking simply runs without Google.',
|
|
325
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
326
|
+
inputSchema: {
|
|
327
|
+
connectionId: z
|
|
328
|
+
.string()
|
|
329
|
+
.optional()
|
|
330
|
+
.describe('Also list the calendars on this connection. Omit to just list the connected accounts.'),
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
async ({ connectionId }) => {
|
|
334
|
+
try {
|
|
335
|
+
const client = makeClient();
|
|
336
|
+
const connections = await client.googleCalendarConnections();
|
|
337
|
+
if (!connectionId) return jsonResult(connections);
|
|
338
|
+
const calendars = await client.googleCalendars(connectionId);
|
|
339
|
+
return jsonResult({ ...connections, ...calendars });
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return errorResult(err);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
server.registerTool(
|
|
347
|
+
'rootr_connect_google_calendar',
|
|
348
|
+
{
|
|
349
|
+
title: 'Start connecting a Google Calendar account',
|
|
350
|
+
description:
|
|
351
|
+
'Get the Google consent URL to connect a Google Calendar account for booking. ' +
|
|
352
|
+
'You cannot finish this for the user: Google requires a PERSON to open that URL in a browser and approve ' +
|
|
353
|
+
'the calendar permissions once. Hand them the URL and say so. After they approve, everything else ' +
|
|
354
|
+
'(listing calendars, wiring them to a table) works through the tools. ' +
|
|
355
|
+
'Rootr asks only for: the list of their calendars, free/busy times, and permission to manage the events it ' +
|
|
356
|
+
'creates — never the contents of their existing events. Needs a human account, not a bare API key.',
|
|
357
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
358
|
+
inputSchema: {
|
|
359
|
+
returnTo: z
|
|
360
|
+
.string()
|
|
361
|
+
.optional()
|
|
362
|
+
.describe('In-app path to come back to after approving, e.g. "/ko". Must start with "/".'),
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
async ({ returnTo }) => {
|
|
366
|
+
try {
|
|
367
|
+
const client = makeClient();
|
|
368
|
+
const res = await client.googleCalendarAuthorizeUrl(returnTo || '/');
|
|
369
|
+
return jsonResult({
|
|
370
|
+
...res,
|
|
371
|
+
next: 'Ask the person to open `url` in a browser and approve. Then call rootr_list_google_calendars.',
|
|
372
|
+
});
|
|
373
|
+
} catch (err) {
|
|
374
|
+
return errorResult(err);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
server.registerTool(
|
|
380
|
+
'rootr_disconnect_google_calendar',
|
|
381
|
+
{
|
|
382
|
+
title: 'Disconnect a Google Calendar account',
|
|
383
|
+
description:
|
|
384
|
+
'Disconnect a Google Calendar account from Rootr (루터) and revoke Rootr\'s access on Google\'s side. ' +
|
|
385
|
+
'Tables using that connection stop blocking busy times and stop writing new bookings to Google; bookings ' +
|
|
386
|
+
'already written stay in the calendar. Ask the person before doing this — it affects every table wired to ' +
|
|
387
|
+
'that account.',
|
|
388
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
389
|
+
inputSchema: {
|
|
390
|
+
connectionId: z.string().describe('Connection id from rootr_list_google_calendars.'),
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
async ({ connectionId }) => {
|
|
394
|
+
try {
|
|
395
|
+
const client = makeClient();
|
|
396
|
+
await client.disconnectGoogleCalendar(connectionId);
|
|
397
|
+
return jsonResult({ ok: true, connectionId });
|
|
398
|
+
} catch (err) {
|
|
399
|
+
return errorResult(err);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
}
|