@anuma/sdk 1.0.0-next.20260224164627
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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/chunk-KDFGY4SK.mjs +13 -0
- package/dist/clientConfig-RMDOT5YM.mjs +10 -0
- package/dist/expo/chunk-LJYAMK62.mjs +25 -0
- package/dist/expo/clientConfig-QDAXFL3W.mjs +10 -0
- package/dist/expo/index.cjs +6899 -0
- package/dist/expo/index.d.mts +2682 -0
- package/dist/expo/index.d.ts +2682 -0
- package/dist/expo/index.mjs +6807 -0
- package/dist/index.cjs +1718 -0
- package/dist/index.d.mts +2949 -0
- package/dist/index.d.ts +2949 -0
- package/dist/index.mjs +1632 -0
- package/dist/next/index.cjs +61 -0
- package/dist/next/index.d.mts +23 -0
- package/dist/next/index.d.ts +23 -0
- package/dist/next/index.mjs +36 -0
- package/dist/polyfills/index.cjs +61 -0
- package/dist/polyfills/index.d.mts +9 -0
- package/dist/polyfills/index.d.ts +9 -0
- package/dist/polyfills/index.mjs +34 -0
- package/dist/react/chunk-LJYAMK62.mjs +25 -0
- package/dist/react/clientConfig-QDAXFL3W.mjs +10 -0
- package/dist/react/index.cjs +15982 -0
- package/dist/react/index.d.mts +6632 -0
- package/dist/react/index.d.ts +6632 -0
- package/dist/react/index.mjs +15733 -0
- package/dist/tools/index.cjs +1698 -0
- package/dist/tools/index.d.mts +427 -0
- package/dist/tools/index.d.ts +427 -0
- package/dist/tools/index.mjs +1645 -0
- package/dist/vercel/index.cjs +86 -0
- package/dist/vercel/index.d.mts +149 -0
- package/dist/vercel/index.d.ts +149 -0
- package/dist/vercel/index.mjs +57 -0
- package/package.json +177 -0
|
@@ -0,0 +1,1645 @@
|
|
|
1
|
+
// src/tools/googleCalendar.ts
|
|
2
|
+
function ensureTimezone(timestamp) {
|
|
3
|
+
if (/[Zz]$/.test(timestamp) || /[+-]\d{2}:\d{2}$/.test(timestamp)) {
|
|
4
|
+
return timestamp;
|
|
5
|
+
}
|
|
6
|
+
return `${timestamp}Z`;
|
|
7
|
+
}
|
|
8
|
+
async function fetchCalendarEvents(accessToken, args) {
|
|
9
|
+
const { timeMin, timeMax, maxResults = 10 } = args;
|
|
10
|
+
const params = new URLSearchParams({
|
|
11
|
+
maxResults: String(maxResults),
|
|
12
|
+
singleEvents: "true",
|
|
13
|
+
orderBy: "startTime"
|
|
14
|
+
});
|
|
15
|
+
const now = /* @__PURE__ */ new Date();
|
|
16
|
+
const defaultTimeMin = now.toISOString();
|
|
17
|
+
const defaultTimeMax = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1e3).toISOString();
|
|
18
|
+
params.set("timeMin", ensureTimezone(timeMin || defaultTimeMin));
|
|
19
|
+
params.set("timeMax", ensureTimezone(timeMax || defaultTimeMax));
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(
|
|
22
|
+
`https://www.googleapis.com/calendar/v3/calendars/primary/events?${params.toString()}`,
|
|
23
|
+
{
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${accessToken}`,
|
|
26
|
+
"Content-Type": "application/json"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
if (response.status === 401) {
|
|
32
|
+
return "Error: Calendar access not authorized. Please grant calendar permissions.";
|
|
33
|
+
}
|
|
34
|
+
const errorText = await response.text();
|
|
35
|
+
return `Error: Failed to fetch calendar events (${response.status}): ${errorText}`;
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
const events = (data.items || []).map(
|
|
39
|
+
(item) => ({
|
|
40
|
+
id: item.id,
|
|
41
|
+
summary: item.summary || "No title",
|
|
42
|
+
start: item.start?.dateTime || item.start?.date || "Unknown",
|
|
43
|
+
end: item.end?.dateTime || item.end?.date || "Unknown",
|
|
44
|
+
description: item.description,
|
|
45
|
+
location: item.location
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
return events;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function buildEventBody(args) {
|
|
54
|
+
const { summary, start, end, description, location, attendees } = args;
|
|
55
|
+
const isAllDay = !start.includes("T");
|
|
56
|
+
const eventBody = {
|
|
57
|
+
summary,
|
|
58
|
+
description,
|
|
59
|
+
location,
|
|
60
|
+
start: isAllDay ? { date: start } : { dateTime: start },
|
|
61
|
+
end: isAllDay ? { date: end } : { dateTime: end }
|
|
62
|
+
};
|
|
63
|
+
if (attendees && attendees.length > 0) {
|
|
64
|
+
eventBody.attendees = attendees.map((email) => ({ email }));
|
|
65
|
+
}
|
|
66
|
+
return eventBody;
|
|
67
|
+
}
|
|
68
|
+
function parseEventResponse(data) {
|
|
69
|
+
return {
|
|
70
|
+
id: data.id,
|
|
71
|
+
summary: data.summary || "No title",
|
|
72
|
+
start: data.start?.dateTime || data.start?.date || "Unknown",
|
|
73
|
+
end: data.end?.dateTime || data.end?.date || "Unknown",
|
|
74
|
+
description: data.description,
|
|
75
|
+
location: data.location
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function createCalendarEvent(accessToken, args) {
|
|
79
|
+
const eventBody = buildEventBody(args);
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(
|
|
82
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
|
|
83
|
+
{
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${accessToken}`,
|
|
87
|
+
"Content-Type": "application/json"
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(eventBody)
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
if (response.status === 401) {
|
|
94
|
+
return "Error: Calendar access not authorized. Please grant calendar permissions.";
|
|
95
|
+
}
|
|
96
|
+
const errorText = await response.text();
|
|
97
|
+
return `Error: Failed to create calendar event (${response.status}): ${errorText}`;
|
|
98
|
+
}
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
return parseEventResponse(data);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function buildUpdateEventBody(args) {
|
|
106
|
+
const { summary, start, end, description, location, attendees } = args;
|
|
107
|
+
const eventBody = {};
|
|
108
|
+
if (summary !== void 0) eventBody.summary = summary;
|
|
109
|
+
if (description !== void 0) eventBody.description = description;
|
|
110
|
+
if (location !== void 0) eventBody.location = location;
|
|
111
|
+
if (start !== void 0) {
|
|
112
|
+
const isAllDay = !start.includes("T");
|
|
113
|
+
eventBody.start = isAllDay ? { date: start } : { dateTime: start };
|
|
114
|
+
}
|
|
115
|
+
if (end !== void 0) {
|
|
116
|
+
const isAllDay = !end.includes("T");
|
|
117
|
+
eventBody.end = isAllDay ? { date: end } : { dateTime: end };
|
|
118
|
+
}
|
|
119
|
+
if (attendees !== void 0) {
|
|
120
|
+
eventBody.attendees = attendees.map((email) => ({ email }));
|
|
121
|
+
}
|
|
122
|
+
return eventBody;
|
|
123
|
+
}
|
|
124
|
+
async function updateCalendarEvent(accessToken, args) {
|
|
125
|
+
const { eventId } = args;
|
|
126
|
+
const eventBody = buildUpdateEventBody(args);
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(
|
|
129
|
+
`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`,
|
|
130
|
+
{
|
|
131
|
+
method: "PATCH",
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${accessToken}`,
|
|
134
|
+
"Content-Type": "application/json"
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(eventBody)
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
if (response.status === 401) {
|
|
141
|
+
return "Error: Calendar access not authorized. Please grant calendar permissions.";
|
|
142
|
+
}
|
|
143
|
+
if (response.status === 404) {
|
|
144
|
+
return `Error: Event not found with ID: ${eventId}`;
|
|
145
|
+
}
|
|
146
|
+
const errorText = await response.text();
|
|
147
|
+
return `Error: Failed to update calendar event (${response.status}): ${errorText}`;
|
|
148
|
+
}
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
return parseEventResponse(data);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function createGoogleCalendarTool(getAccessToken, requestCalendarAccess) {
|
|
156
|
+
return {
|
|
157
|
+
type: "function",
|
|
158
|
+
function: {
|
|
159
|
+
name: "google_calendar_list_events",
|
|
160
|
+
description: "Lists upcoming events from the user's Google Calendar. Returns events within a specified time range. If no time range is specified, returns events for the next 7 days.",
|
|
161
|
+
arguments: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
timeMin: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: 'Start of the time range (ISO 8601 format, e.g., "2024-01-15T00:00:00Z"). Defaults to now.'
|
|
167
|
+
},
|
|
168
|
+
timeMax: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: 'End of the time range (ISO 8601 format, e.g., "2024-01-22T23:59:59Z"). Defaults to 7 days from now.'
|
|
171
|
+
},
|
|
172
|
+
maxResults: {
|
|
173
|
+
type: "number",
|
|
174
|
+
description: "Maximum number of events to return. Defaults to 10."
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
required: []
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
executor: async (args) => {
|
|
181
|
+
let token = getAccessToken();
|
|
182
|
+
if (!token) {
|
|
183
|
+
try {
|
|
184
|
+
token = await requestCalendarAccess();
|
|
185
|
+
} catch {
|
|
186
|
+
return "Error: Failed to get calendar access. Please grant permissions when prompted.";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!token) {
|
|
190
|
+
return "Error: No Google Calendar access token available. Please connect your Google account.";
|
|
191
|
+
}
|
|
192
|
+
const typedArgs = {
|
|
193
|
+
timeMin: args.timeMin,
|
|
194
|
+
timeMax: args.timeMax,
|
|
195
|
+
maxResults: args.maxResults
|
|
196
|
+
};
|
|
197
|
+
return fetchCalendarEvents(token, typedArgs);
|
|
198
|
+
},
|
|
199
|
+
autoExecute: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function createGoogleCalendarCreateEventTool(getAccessToken, requestCalendarAccess) {
|
|
203
|
+
return {
|
|
204
|
+
type: "function",
|
|
205
|
+
function: {
|
|
206
|
+
name: "google_calendar_create_event",
|
|
207
|
+
description: "Creates a new event in the user's Google Calendar. Supports both timed events (with specific times) and all-day events (date only).",
|
|
208
|
+
arguments: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
summary: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "The title/name of the event."
|
|
214
|
+
},
|
|
215
|
+
start: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: 'Start time in ISO 8601 format. For timed events use datetime format (e.g., "2024-01-15T09:00:00-05:00"). For all-day events use date format (e.g., "2024-01-15").'
|
|
218
|
+
},
|
|
219
|
+
end: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: 'End time in ISO 8601 format. For timed events use datetime format (e.g., "2024-01-15T10:00:00-05:00"). For all-day events use date format (e.g., "2024-01-16", note: end date is exclusive).'
|
|
222
|
+
},
|
|
223
|
+
description: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Optional description or notes for the event."
|
|
226
|
+
},
|
|
227
|
+
location: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description: "Optional location for the event."
|
|
230
|
+
},
|
|
231
|
+
attendees: {
|
|
232
|
+
type: "array",
|
|
233
|
+
items: { type: "string" },
|
|
234
|
+
description: "Optional list of email addresses to invite as attendees."
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
required: ["summary", "start", "end"]
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
executor: async (args) => {
|
|
241
|
+
let token = getAccessToken();
|
|
242
|
+
if (!token) {
|
|
243
|
+
try {
|
|
244
|
+
token = await requestCalendarAccess();
|
|
245
|
+
} catch {
|
|
246
|
+
return "Error: Failed to get calendar access. Please grant permissions when prompted.";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!token) {
|
|
250
|
+
return "Error: No Google Calendar access token available. Please connect your Google account.";
|
|
251
|
+
}
|
|
252
|
+
const typedArgs = {
|
|
253
|
+
summary: args.summary,
|
|
254
|
+
start: args.start,
|
|
255
|
+
end: args.end,
|
|
256
|
+
description: args.description,
|
|
257
|
+
location: args.location,
|
|
258
|
+
attendees: args.attendees
|
|
259
|
+
};
|
|
260
|
+
return createCalendarEvent(token, typedArgs);
|
|
261
|
+
},
|
|
262
|
+
autoExecute: true
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function createGoogleCalendarUpdateEventTool(getAccessToken, requestCalendarAccess) {
|
|
266
|
+
return {
|
|
267
|
+
type: "function",
|
|
268
|
+
function: {
|
|
269
|
+
name: "google_calendar_update_event",
|
|
270
|
+
description: "Updates an existing event in the user's Google Calendar. Only the fields provided will be updated; other fields remain unchanged. Use google_calendar_list_events first to get the event ID.",
|
|
271
|
+
arguments: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: {
|
|
274
|
+
eventId: {
|
|
275
|
+
type: "string",
|
|
276
|
+
description: "The ID of the event to update (obtained from google_calendar_list_events)."
|
|
277
|
+
},
|
|
278
|
+
summary: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "The new title/name of the event."
|
|
281
|
+
},
|
|
282
|
+
start: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: 'New start time in ISO 8601 format. For timed events use datetime format (e.g., "2024-01-15T09:00:00-05:00"). For all-day events use date format (e.g., "2024-01-15").'
|
|
285
|
+
},
|
|
286
|
+
end: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: 'New end time in ISO 8601 format. For timed events use datetime format (e.g., "2024-01-15T10:00:00-05:00"). For all-day events use date format (e.g., "2024-01-16").'
|
|
289
|
+
},
|
|
290
|
+
description: {
|
|
291
|
+
type: "string",
|
|
292
|
+
description: "New description or notes for the event."
|
|
293
|
+
},
|
|
294
|
+
location: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: "New location for the event."
|
|
297
|
+
},
|
|
298
|
+
attendees: {
|
|
299
|
+
type: "array",
|
|
300
|
+
items: { type: "string" },
|
|
301
|
+
description: "New list of email addresses to invite as attendees (replaces existing attendees)."
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
required: ["eventId"]
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
executor: async (args) => {
|
|
308
|
+
let token = getAccessToken();
|
|
309
|
+
if (!token) {
|
|
310
|
+
try {
|
|
311
|
+
token = await requestCalendarAccess();
|
|
312
|
+
} catch {
|
|
313
|
+
return "Error: Failed to get calendar access. Please grant permissions when prompted.";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!token) {
|
|
317
|
+
return "Error: No Google Calendar access token available. Please connect your Google account.";
|
|
318
|
+
}
|
|
319
|
+
const typedArgs = {
|
|
320
|
+
eventId: args.eventId,
|
|
321
|
+
summary: args.summary,
|
|
322
|
+
start: args.start,
|
|
323
|
+
end: args.end,
|
|
324
|
+
description: args.description,
|
|
325
|
+
location: args.location,
|
|
326
|
+
attendees: args.attendees
|
|
327
|
+
};
|
|
328
|
+
return updateCalendarEvent(token, typedArgs);
|
|
329
|
+
},
|
|
330
|
+
autoExecute: true
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function createChatTools(getAccessToken, requestCalendarAccess) {
|
|
334
|
+
return [
|
|
335
|
+
createGoogleCalendarTool(getAccessToken, requestCalendarAccess),
|
|
336
|
+
createGoogleCalendarCreateEventTool(getAccessToken, requestCalendarAccess),
|
|
337
|
+
createGoogleCalendarUpdateEventTool(getAccessToken, requestCalendarAccess)
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/tools/uiInteraction.ts
|
|
342
|
+
function migrateDisplayResult(result, fromVersion, toVersion, migrations) {
|
|
343
|
+
if (fromVersion >= toVersion) return result;
|
|
344
|
+
let current = result;
|
|
345
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
346
|
+
const key = `${v}->${v + 1}`;
|
|
347
|
+
const migrate = migrations[key];
|
|
348
|
+
if (migrate) {
|
|
349
|
+
current = migrate(current);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return current;
|
|
353
|
+
}
|
|
354
|
+
function generateInteractionId(prefix) {
|
|
355
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
356
|
+
}
|
|
357
|
+
function createInteractiveTool(options, config) {
|
|
358
|
+
return {
|
|
359
|
+
type: "function",
|
|
360
|
+
function: {
|
|
361
|
+
name: config.name,
|
|
362
|
+
description: config.description,
|
|
363
|
+
arguments: config.parameters
|
|
364
|
+
},
|
|
365
|
+
executor: async (args) => {
|
|
366
|
+
if (config.validate && !config.validate(args)) {
|
|
367
|
+
return { cancelled: true };
|
|
368
|
+
}
|
|
369
|
+
const context = options.getContext();
|
|
370
|
+
if (!context) {
|
|
371
|
+
return { cancelled: true };
|
|
372
|
+
}
|
|
373
|
+
const interactionId = generateInteractionId(config.interactionType);
|
|
374
|
+
const interactionData = config.mapArgs ? config.mapArgs(args) : args;
|
|
375
|
+
try {
|
|
376
|
+
const result = await context.createInteraction(interactionId, config.interactionType, {
|
|
377
|
+
...interactionData,
|
|
378
|
+
afterMessageId: options.getLastMessageId?.()
|
|
379
|
+
});
|
|
380
|
+
if (config.mapResult) {
|
|
381
|
+
return config.mapResult(result, args);
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
} catch {
|
|
385
|
+
return { cancelled: true };
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
// Interactive tools block until the user responds (e.g. fills a form,
|
|
389
|
+
// picks a choice). They must NOT be auto-executed by the SDK's internal
|
|
390
|
+
// tool loop — instead the app's own onToolCall handler should drive them
|
|
391
|
+
// so the result is persisted as a "[Tool Execution Results]" message.
|
|
392
|
+
autoExecute: false
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function createDisplayTool(options, config) {
|
|
396
|
+
return {
|
|
397
|
+
type: "function",
|
|
398
|
+
function: {
|
|
399
|
+
name: config.name,
|
|
400
|
+
description: config.description,
|
|
401
|
+
arguments: config.parameters
|
|
402
|
+
},
|
|
403
|
+
executor: async (args) => {
|
|
404
|
+
const result = await config.execute(args);
|
|
405
|
+
const context = options.getContext();
|
|
406
|
+
if (context) {
|
|
407
|
+
const interactionId = generateInteractionId(config.displayType);
|
|
408
|
+
context.createDisplayInteraction(
|
|
409
|
+
interactionId,
|
|
410
|
+
config.displayType,
|
|
411
|
+
{ afterMessageId: options.getLastMessageId?.() },
|
|
412
|
+
result,
|
|
413
|
+
config.version ?? 1
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
},
|
|
418
|
+
autoExecute: true,
|
|
419
|
+
skipContinuation: true
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/tools/chart.ts
|
|
424
|
+
function createChartTool(options) {
|
|
425
|
+
return createDisplayTool(options, {
|
|
426
|
+
name: "display_chart",
|
|
427
|
+
description: "Renders a chart inline in the chat. Supports bar, line, area, and pie charts. RULES: (1) Call this tool ONCE per request \u2014 you cannot update a chart after it renders. (2) When the user provides data in their message, call this tool immediately with that data \u2014 do NOT search or verify it. (3) Only search/fetch if the user asks for data you don't have. (4) Do NOT repeat the chart data as text in your response. Just add a brief conversational comment about the chart. Use simple alphanumeric keys without spaces (e.g. 'revenue', 'users', 'q1Sales').",
|
|
428
|
+
parameters: {
|
|
429
|
+
type: "object",
|
|
430
|
+
properties: {
|
|
431
|
+
chartType: {
|
|
432
|
+
type: "string",
|
|
433
|
+
enum: ["bar", "line", "area", "pie"],
|
|
434
|
+
description: "Type of chart to render"
|
|
435
|
+
},
|
|
436
|
+
title: {
|
|
437
|
+
type: "string",
|
|
438
|
+
description: "Optional title displayed above the chart"
|
|
439
|
+
},
|
|
440
|
+
data: {
|
|
441
|
+
type: "array",
|
|
442
|
+
items: {
|
|
443
|
+
type: "object",
|
|
444
|
+
additionalProperties: true
|
|
445
|
+
},
|
|
446
|
+
description: "Array of data points. Each object should have a label key and one or more numeric value keys."
|
|
447
|
+
},
|
|
448
|
+
dataKeys: {
|
|
449
|
+
type: "array",
|
|
450
|
+
items: { type: "string" },
|
|
451
|
+
description: "Which keys in each data object to chart as series/bars/slices (the numeric values)."
|
|
452
|
+
},
|
|
453
|
+
xAxisKey: {
|
|
454
|
+
type: "string",
|
|
455
|
+
description: "Which key in each data object to use for x-axis labels (for bar, line, area charts). For pie charts, this is the name/label key for each slice."
|
|
456
|
+
},
|
|
457
|
+
colors: {
|
|
458
|
+
type: "object",
|
|
459
|
+
additionalProperties: { type: "string" },
|
|
460
|
+
description: "Optional color overrides. Map of dataKey to CSS color value (e.g. { 'revenue': '#2563eb' }). If omitted, uses theme chart colors."
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
required: ["chartType", "data", "dataKeys"]
|
|
464
|
+
},
|
|
465
|
+
displayType: "chart",
|
|
466
|
+
execute: async (args) => {
|
|
467
|
+
const chartType = args.chartType;
|
|
468
|
+
const data = args.data;
|
|
469
|
+
const dataKeys = args.dataKeys;
|
|
470
|
+
if (!chartType || !["bar", "line", "area", "pie"].includes(chartType)) {
|
|
471
|
+
return { error: `Unsupported chart type: ${chartType}` };
|
|
472
|
+
}
|
|
473
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
474
|
+
return { error: "Invalid or empty chart data" };
|
|
475
|
+
}
|
|
476
|
+
if (!dataKeys || !Array.isArray(dataKeys) || dataKeys.length === 0) {
|
|
477
|
+
return { error: "No data keys specified for charting" };
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
chartType,
|
|
481
|
+
title: args.title,
|
|
482
|
+
data,
|
|
483
|
+
dataKeys,
|
|
484
|
+
xAxisKey: args.xAxisKey,
|
|
485
|
+
colors: args.colors
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/tools/googleDrive.ts
|
|
492
|
+
async function searchDriveFiles(accessToken, args) {
|
|
493
|
+
const { query, maxResults = 10, mimeType } = args;
|
|
494
|
+
let driveQuery = `fullText contains '${query.replace(/'/g, "\\'")}'`;
|
|
495
|
+
if (mimeType) {
|
|
496
|
+
driveQuery += ` and mimeType = '${mimeType}'`;
|
|
497
|
+
}
|
|
498
|
+
driveQuery += " and trashed = false";
|
|
499
|
+
const params = new URLSearchParams({
|
|
500
|
+
q: driveQuery,
|
|
501
|
+
pageSize: String(maxResults),
|
|
502
|
+
fields: "files(id,name,mimeType,webViewLink,modifiedTime,size,owners)",
|
|
503
|
+
orderBy: "modifiedTime desc"
|
|
504
|
+
});
|
|
505
|
+
try {
|
|
506
|
+
const response = await fetch(`https://www.googleapis.com/drive/v3/files?${params.toString()}`, {
|
|
507
|
+
headers: {
|
|
508
|
+
Authorization: `Bearer ${accessToken}`,
|
|
509
|
+
"Content-Type": "application/json"
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
if (response.status === 401) {
|
|
514
|
+
return "Error: Google Drive access not authorized. Please grant Drive permissions.";
|
|
515
|
+
}
|
|
516
|
+
if (response.status === 403) {
|
|
517
|
+
return "Error: Insufficient permissions to access Google Drive. Please grant read access to your Drive.";
|
|
518
|
+
}
|
|
519
|
+
const errorText = await response.text();
|
|
520
|
+
return `Error: Failed to search Google Drive (${response.status}): ${errorText}`;
|
|
521
|
+
}
|
|
522
|
+
const data = await response.json();
|
|
523
|
+
const files = (data.files || []).map((file) => ({
|
|
524
|
+
id: file.id,
|
|
525
|
+
name: file.name,
|
|
526
|
+
mimeType: file.mimeType,
|
|
527
|
+
webViewLink: file.webViewLink,
|
|
528
|
+
modifiedTime: file.modifiedTime,
|
|
529
|
+
size: file.size,
|
|
530
|
+
owners: file.owners
|
|
531
|
+
}));
|
|
532
|
+
if (files.length === 0) {
|
|
533
|
+
return `No files found matching "${query}"`;
|
|
534
|
+
}
|
|
535
|
+
return files;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function createGoogleDriveSearchTool(getAccessToken, requestDriveAccess) {
|
|
541
|
+
return {
|
|
542
|
+
type: "function",
|
|
543
|
+
function: {
|
|
544
|
+
name: "google_drive_search",
|
|
545
|
+
description: "Searches for files in the user's Google Drive. Returns matching files with their names, types, and links. Use this to help users find documents, spreadsheets, presentations, PDFs, and other files stored in their Drive.",
|
|
546
|
+
arguments: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {
|
|
549
|
+
query: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: 'The search query to find files. Searches in file names and content. Examples: "quarterly report", "budget 2024", "meeting notes".'
|
|
552
|
+
},
|
|
553
|
+
maxResults: {
|
|
554
|
+
type: "number",
|
|
555
|
+
description: "Maximum number of files to return. Defaults to 10. Maximum is 100."
|
|
556
|
+
},
|
|
557
|
+
mimeType: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: 'Optional MIME type filter. Common types: "application/vnd.google-apps.document" (Google Docs), "application/vnd.google-apps.spreadsheet" (Sheets), "application/vnd.google-apps.presentation" (Slides), "application/pdf" (PDF files).'
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
required: ["query"]
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
executor: async (args) => {
|
|
566
|
+
let token = getAccessToken();
|
|
567
|
+
if (!token) {
|
|
568
|
+
try {
|
|
569
|
+
token = await requestDriveAccess();
|
|
570
|
+
} catch {
|
|
571
|
+
return "Error: Failed to get Google Drive access. Please grant permissions when prompted.";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (!token) {
|
|
575
|
+
return "Error: No Google Drive access token available. Please connect your Google account.";
|
|
576
|
+
}
|
|
577
|
+
const typedArgs = {
|
|
578
|
+
query: args.query,
|
|
579
|
+
maxResults: args.maxResults,
|
|
580
|
+
mimeType: args.mimeType
|
|
581
|
+
};
|
|
582
|
+
return searchDriveFiles(token, typedArgs);
|
|
583
|
+
},
|
|
584
|
+
autoExecute: true
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async function listRecentDriveFiles(accessToken, args) {
|
|
588
|
+
const { maxResults = 10, mimeType } = args;
|
|
589
|
+
let driveQuery = "trashed = false";
|
|
590
|
+
if (mimeType) {
|
|
591
|
+
driveQuery += ` and mimeType = '${mimeType}'`;
|
|
592
|
+
}
|
|
593
|
+
const params = new URLSearchParams({
|
|
594
|
+
q: driveQuery,
|
|
595
|
+
pageSize: String(maxResults),
|
|
596
|
+
fields: "files(id,name,mimeType,webViewLink,modifiedTime,size,owners)",
|
|
597
|
+
orderBy: "modifiedTime desc"
|
|
598
|
+
});
|
|
599
|
+
try {
|
|
600
|
+
const response = await fetch(`https://www.googleapis.com/drive/v3/files?${params.toString()}`, {
|
|
601
|
+
headers: {
|
|
602
|
+
Authorization: `Bearer ${accessToken}`,
|
|
603
|
+
"Content-Type": "application/json"
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
if (!response.ok) {
|
|
607
|
+
if (response.status === 401) {
|
|
608
|
+
return "Error: Google Drive access not authorized. Please grant Drive permissions.";
|
|
609
|
+
}
|
|
610
|
+
if (response.status === 403) {
|
|
611
|
+
return "Error: Insufficient permissions to access Google Drive. Please grant read access to your Drive.";
|
|
612
|
+
}
|
|
613
|
+
const errorText = await response.text();
|
|
614
|
+
return `Error: Failed to list Google Drive files (${response.status}): ${errorText}`;
|
|
615
|
+
}
|
|
616
|
+
const data = await response.json();
|
|
617
|
+
const files = (data.files || []).map((file) => ({
|
|
618
|
+
id: file.id,
|
|
619
|
+
name: file.name,
|
|
620
|
+
mimeType: file.mimeType,
|
|
621
|
+
webViewLink: file.webViewLink,
|
|
622
|
+
modifiedTime: file.modifiedTime,
|
|
623
|
+
size: file.size,
|
|
624
|
+
owners: file.owners
|
|
625
|
+
}));
|
|
626
|
+
if (files.length === 0) {
|
|
627
|
+
return "No recent files found in your Google Drive.";
|
|
628
|
+
}
|
|
629
|
+
return files;
|
|
630
|
+
} catch (error) {
|
|
631
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function createGoogleDriveListRecentTool(getAccessToken, requestDriveAccess) {
|
|
635
|
+
return {
|
|
636
|
+
type: "function",
|
|
637
|
+
function: {
|
|
638
|
+
name: "google_drive_list_recent",
|
|
639
|
+
description: "Lists recent files from the user's Google Drive, ordered by last modified date. Use this when the user wants to see their recent files without a specific search query.",
|
|
640
|
+
arguments: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: {
|
|
643
|
+
maxResults: {
|
|
644
|
+
type: "number",
|
|
645
|
+
description: "Maximum number of files to return. Defaults to 10. Maximum is 100."
|
|
646
|
+
},
|
|
647
|
+
mimeType: {
|
|
648
|
+
type: "string",
|
|
649
|
+
description: 'Optional MIME type filter. Common types: "application/vnd.google-apps.document" (Google Docs), "application/vnd.google-apps.spreadsheet" (Sheets), "application/vnd.google-apps.presentation" (Slides), "application/pdf" (PDF files).'
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
required: []
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
executor: async (args) => {
|
|
656
|
+
let token = getAccessToken();
|
|
657
|
+
if (!token) {
|
|
658
|
+
try {
|
|
659
|
+
token = await requestDriveAccess();
|
|
660
|
+
} catch {
|
|
661
|
+
return "Error: Failed to get Google Drive access. Please grant permissions when prompted.";
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (!token) {
|
|
665
|
+
return "Error: No Google Drive access token available. Please connect your Google account.";
|
|
666
|
+
}
|
|
667
|
+
const typedArgs = {
|
|
668
|
+
maxResults: args.maxResults,
|
|
669
|
+
mimeType: args.mimeType
|
|
670
|
+
};
|
|
671
|
+
return listRecentDriveFiles(token, typedArgs);
|
|
672
|
+
},
|
|
673
|
+
autoExecute: true
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function findFileByName(accessToken, fileName) {
|
|
677
|
+
const driveQuery = `name contains '${fileName.replace(/'/g, "\\'")}' and trashed = false`;
|
|
678
|
+
const params = new URLSearchParams({
|
|
679
|
+
q: driveQuery,
|
|
680
|
+
pageSize: "1",
|
|
681
|
+
fields: "files(id,name)",
|
|
682
|
+
orderBy: "modifiedTime desc"
|
|
683
|
+
});
|
|
684
|
+
const response = await fetch(`https://www.googleapis.com/drive/v3/files?${params.toString()}`, {
|
|
685
|
+
headers: {
|
|
686
|
+
Authorization: `Bearer ${accessToken}`,
|
|
687
|
+
"Content-Type": "application/json"
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
if (!response.ok) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
const data = await response.json();
|
|
694
|
+
return data.files?.[0]?.id || null;
|
|
695
|
+
}
|
|
696
|
+
var GOOGLE_DOCS_EXPORT_TYPES = {
|
|
697
|
+
"application/vnd.google-apps.document": "text/plain",
|
|
698
|
+
"application/vnd.google-apps.spreadsheet": "text/csv",
|
|
699
|
+
"application/vnd.google-apps.presentation": "text/plain"
|
|
700
|
+
};
|
|
701
|
+
var MAX_CONTENT_LENGTH = 5e4;
|
|
702
|
+
async function resolveFileId(accessToken, fileId, fileName) {
|
|
703
|
+
if (fileId) {
|
|
704
|
+
return { fileId };
|
|
705
|
+
}
|
|
706
|
+
if (fileName) {
|
|
707
|
+
console.log("[google_drive_get_content] Searching for file by name:", fileName);
|
|
708
|
+
const foundId = await findFileByName(accessToken, fileName);
|
|
709
|
+
if (!foundId) {
|
|
710
|
+
return {
|
|
711
|
+
error: `Error: Could not find a file matching "${fileName}". Please check the file name and try again.`
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
console.log("[google_drive_get_content] Found file ID:", foundId);
|
|
715
|
+
return { fileId: foundId };
|
|
716
|
+
}
|
|
717
|
+
return { error: "Error: Please provide either a fileId or fileName to get file content." };
|
|
718
|
+
}
|
|
719
|
+
async function fetchFileMetadata(accessToken, fileId) {
|
|
720
|
+
const response = await fetch(
|
|
721
|
+
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,webViewLink`,
|
|
722
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
723
|
+
);
|
|
724
|
+
if (!response.ok) {
|
|
725
|
+
if (response.status === 404) {
|
|
726
|
+
return { error: "Error: File not found. Please check the file ID." };
|
|
727
|
+
}
|
|
728
|
+
return { error: `Error: Failed to get file metadata (${response.status})` };
|
|
729
|
+
}
|
|
730
|
+
const data = await response.json();
|
|
731
|
+
return {
|
|
732
|
+
metadata: { name: data.name, mimeType: data.mimeType, webViewLink: data.webViewLink }
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function getContentUrl(fileId, mimeType) {
|
|
736
|
+
if (GOOGLE_DOCS_EXPORT_TYPES[mimeType]) {
|
|
737
|
+
const exportMimeType = GOOGLE_DOCS_EXPORT_TYPES[mimeType];
|
|
738
|
+
return {
|
|
739
|
+
url: `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(exportMimeType)}`,
|
|
740
|
+
isExport: true
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml") {
|
|
744
|
+
return {
|
|
745
|
+
url: `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`,
|
|
746
|
+
isExport: false
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
function formatNonTextFileResponse(fileId, metadata) {
|
|
752
|
+
const { name, mimeType, webViewLink } = metadata;
|
|
753
|
+
const linkInfo = webViewLink ? `
|
|
754
|
+
View in Google Drive: ${webViewLink}` : "";
|
|
755
|
+
if (mimeType === "application/pdf") {
|
|
756
|
+
return `File: ${name}
|
|
757
|
+
Type: PDF${linkInfo}
|
|
758
|
+
|
|
759
|
+
Note: PDF content cannot be extracted directly in the browser. The user can click the link above to view the PDF in Google Drive.`;
|
|
760
|
+
}
|
|
761
|
+
if (mimeType.startsWith("image/")) {
|
|
762
|
+
const directUrl = `https://drive.google.com/uc?export=view&id=${fileId}`;
|
|
763
|
+
return `File: ${name}
|
|
764
|
+
Type: ${mimeType}${linkInfo}
|
|
765
|
+
Direct image URL: ${directUrl}
|
|
766
|
+
|
|
767
|
+
To view this image, click the Google Drive link above or use the direct URL.`;
|
|
768
|
+
}
|
|
769
|
+
return `File: ${name}
|
|
770
|
+
Type: ${mimeType}${linkInfo}
|
|
771
|
+
|
|
772
|
+
Note: This file type cannot be displayed as text.`;
|
|
773
|
+
}
|
|
774
|
+
async function fetchAndFormatContent(accessToken, contentUrl, metadata, isExport) {
|
|
775
|
+
const response = await fetch(contentUrl, {
|
|
776
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
777
|
+
});
|
|
778
|
+
if (!response.ok) {
|
|
779
|
+
if (response.status === 403) {
|
|
780
|
+
return `Error: Permission denied. You may not have access to view this file's content.`;
|
|
781
|
+
}
|
|
782
|
+
return `Error: Failed to get file content (${response.status})`;
|
|
783
|
+
}
|
|
784
|
+
const content = await response.text();
|
|
785
|
+
const truncated = content.length > MAX_CONTENT_LENGTH;
|
|
786
|
+
const displayContent = truncated ? content.slice(0, MAX_CONTENT_LENGTH) : content;
|
|
787
|
+
const header = `=== File: ${metadata.name} ===
|
|
788
|
+
Type: ${metadata.mimeType}${isExport ? " (exported as text)" : ""}
|
|
789
|
+
|
|
790
|
+
`;
|
|
791
|
+
const footer = truncated ? `
|
|
792
|
+
|
|
793
|
+
... (content truncated, showing first ${MAX_CONTENT_LENGTH} characters of ${content.length})` : "";
|
|
794
|
+
return header + displayContent + footer;
|
|
795
|
+
}
|
|
796
|
+
async function getDriveFileContent(accessToken, args) {
|
|
797
|
+
console.log("[google_drive_get_content] Starting with:", args);
|
|
798
|
+
const fileIdResult = await resolveFileId(accessToken, args.fileId, args.fileName);
|
|
799
|
+
if ("error" in fileIdResult) {
|
|
800
|
+
return fileIdResult.error;
|
|
801
|
+
}
|
|
802
|
+
const { fileId } = fileIdResult;
|
|
803
|
+
try {
|
|
804
|
+
const metadataResult = await fetchFileMetadata(accessToken, fileId);
|
|
805
|
+
if ("error" in metadataResult) {
|
|
806
|
+
return metadataResult.error;
|
|
807
|
+
}
|
|
808
|
+
const { metadata } = metadataResult;
|
|
809
|
+
console.log("[google_drive_get_content] File metadata:", metadata);
|
|
810
|
+
const contentUrlResult = getContentUrl(fileId, metadata.mimeType);
|
|
811
|
+
if (!contentUrlResult) {
|
|
812
|
+
return formatNonTextFileResponse(fileId, metadata);
|
|
813
|
+
}
|
|
814
|
+
return fetchAndFormatContent(
|
|
815
|
+
accessToken,
|
|
816
|
+
contentUrlResult.url,
|
|
817
|
+
metadata,
|
|
818
|
+
contentUrlResult.isExport
|
|
819
|
+
);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
return `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function createGoogleDriveGetContentTool(getAccessToken, requestDriveAccess) {
|
|
825
|
+
return {
|
|
826
|
+
type: "function",
|
|
827
|
+
function: {
|
|
828
|
+
name: "google_drive_get_content",
|
|
829
|
+
description: "Gets the text content of a file from the user's Google Drive. Works with Google Docs, Sheets, Slides (exported as text/CSV), and text-based files. You can provide either a fileId (from a previous search) or a fileName to search for.",
|
|
830
|
+
arguments: {
|
|
831
|
+
type: "object",
|
|
832
|
+
properties: {
|
|
833
|
+
fileId: {
|
|
834
|
+
type: "string",
|
|
835
|
+
description: "The unique file ID from Google Drive. Use this if you have the ID from a previous search result."
|
|
836
|
+
},
|
|
837
|
+
fileName: {
|
|
838
|
+
type: "string",
|
|
839
|
+
description: "The name (or partial name) of the file to find and read. Use this when you know the file name but not the ID."
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
required: []
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
executor: async (args) => {
|
|
846
|
+
console.log("[google_drive_get_content] Executor called with args:", args);
|
|
847
|
+
let token = getAccessToken();
|
|
848
|
+
if (!token) {
|
|
849
|
+
console.log("[google_drive_get_content] No token, requesting access...");
|
|
850
|
+
try {
|
|
851
|
+
token = await requestDriveAccess();
|
|
852
|
+
} catch {
|
|
853
|
+
return "Error: Failed to get Google Drive access. Please grant permissions when prompted.";
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (!token) {
|
|
857
|
+
return "Error: No Google Drive access token available. Please connect your Google account.";
|
|
858
|
+
}
|
|
859
|
+
const typedArgs = {
|
|
860
|
+
fileId: args.fileId,
|
|
861
|
+
fileName: args.fileName
|
|
862
|
+
};
|
|
863
|
+
const result = await getDriveFileContent(token, typedArgs);
|
|
864
|
+
console.log(
|
|
865
|
+
"[google_drive_get_content] Result length:",
|
|
866
|
+
result.length,
|
|
867
|
+
"First 200 chars:",
|
|
868
|
+
result.slice(0, 200)
|
|
869
|
+
);
|
|
870
|
+
return result;
|
|
871
|
+
},
|
|
872
|
+
autoExecute: true
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function createDriveTools(getAccessToken, requestDriveAccess) {
|
|
876
|
+
return [
|
|
877
|
+
createGoogleDriveSearchTool(getAccessToken, requestDriveAccess),
|
|
878
|
+
createGoogleDriveListRecentTool(getAccessToken, requestDriveAccess),
|
|
879
|
+
createGoogleDriveGetContentTool(getAccessToken, requestDriveAccess)
|
|
880
|
+
];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/tools/notion.ts
|
|
884
|
+
var MCP_HTTP_ENDPOINT = "https://mcp.notion.com/mcp";
|
|
885
|
+
var MCP_SSE_ENDPOINT = "https://mcp.notion.com/sse";
|
|
886
|
+
var requestIdCounter = Math.floor(Math.random() * 1e6);
|
|
887
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
888
|
+
function generateRequestId() {
|
|
889
|
+
return ++requestIdCounter;
|
|
890
|
+
}
|
|
891
|
+
function parseSSEResponse(text) {
|
|
892
|
+
const events = text.split(/\n\n+/);
|
|
893
|
+
let lastEventData = "";
|
|
894
|
+
for (const event of events) {
|
|
895
|
+
const dataLines = [];
|
|
896
|
+
for (const line of event.split("\n")) {
|
|
897
|
+
if (line.startsWith("data: ")) {
|
|
898
|
+
dataLines.push(line.slice(6));
|
|
899
|
+
} else if (line.startsWith("data:")) {
|
|
900
|
+
dataLines.push(line.slice(5));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (dataLines.length > 0) {
|
|
904
|
+
lastEventData = dataLines.join("\n");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (!lastEventData) {
|
|
908
|
+
throw new Error("No data found in SSE response");
|
|
909
|
+
}
|
|
910
|
+
return JSON.parse(lastEventData);
|
|
911
|
+
}
|
|
912
|
+
async function parseResponseBody(response) {
|
|
913
|
+
const contentType = response.headers.get("Content-Type") || "";
|
|
914
|
+
const text = await response.text();
|
|
915
|
+
if (contentType.includes("text/event-stream") || text.startsWith("event:") || text.startsWith("data:")) {
|
|
916
|
+
return parseSSEResponse(text);
|
|
917
|
+
}
|
|
918
|
+
return JSON.parse(text);
|
|
919
|
+
}
|
|
920
|
+
async function initializeMCPSession(accessToken) {
|
|
921
|
+
const requestId = generateRequestId();
|
|
922
|
+
const initRequest = {
|
|
923
|
+
jsonrpc: "2.0",
|
|
924
|
+
id: requestId,
|
|
925
|
+
method: "initialize",
|
|
926
|
+
params: {
|
|
927
|
+
protocolVersion: "2024-11-05",
|
|
928
|
+
capabilities: {},
|
|
929
|
+
clientInfo: {
|
|
930
|
+
name: "Anuma",
|
|
931
|
+
version: "1.0.0"
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
const response = await fetch(MCP_HTTP_ENDPOINT, {
|
|
936
|
+
method: "POST",
|
|
937
|
+
headers: {
|
|
938
|
+
"Content-Type": "application/json",
|
|
939
|
+
Accept: "application/json, text/event-stream",
|
|
940
|
+
Authorization: `Bearer ${accessToken}`
|
|
941
|
+
},
|
|
942
|
+
body: JSON.stringify(initRequest)
|
|
943
|
+
});
|
|
944
|
+
if (!response.ok) {
|
|
945
|
+
const errorBody = await response.text().catch(() => "");
|
|
946
|
+
throw new Error(
|
|
947
|
+
`MCP initialization failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const sessionId = response.headers.get("Mcp-Session-Id");
|
|
951
|
+
if (!sessionId) {
|
|
952
|
+
throw new Error("No Mcp-Session-Id returned from initialization");
|
|
953
|
+
}
|
|
954
|
+
const jsonRpcResponse = await parseResponseBody(response);
|
|
955
|
+
if (jsonRpcResponse.error) {
|
|
956
|
+
const err = jsonRpcResponse.error;
|
|
957
|
+
throw new Error(`MCP initialization error: ${err.message || JSON.stringify(err)}`);
|
|
958
|
+
}
|
|
959
|
+
await fetch(MCP_HTTP_ENDPOINT, {
|
|
960
|
+
method: "POST",
|
|
961
|
+
headers: {
|
|
962
|
+
"Content-Type": "application/json",
|
|
963
|
+
Authorization: `Bearer ${accessToken}`,
|
|
964
|
+
"Mcp-Session-Id": sessionId
|
|
965
|
+
},
|
|
966
|
+
body: JSON.stringify({
|
|
967
|
+
jsonrpc: "2.0",
|
|
968
|
+
method: "notifications/initialized"
|
|
969
|
+
})
|
|
970
|
+
});
|
|
971
|
+
sessionCache.set(accessToken, sessionId);
|
|
972
|
+
return sessionId;
|
|
973
|
+
}
|
|
974
|
+
async function ensureMCPSession(accessToken) {
|
|
975
|
+
const cached = sessionCache.get(accessToken);
|
|
976
|
+
if (cached) {
|
|
977
|
+
return cached;
|
|
978
|
+
}
|
|
979
|
+
return initializeMCPSession(accessToken);
|
|
980
|
+
}
|
|
981
|
+
async function callMCPTool(accessToken, toolName, args) {
|
|
982
|
+
const sessionId = await ensureMCPSession(accessToken);
|
|
983
|
+
const requestId = generateRequestId();
|
|
984
|
+
const jsonRpcRequest = {
|
|
985
|
+
jsonrpc: "2.0",
|
|
986
|
+
id: requestId,
|
|
987
|
+
method: "tools/call",
|
|
988
|
+
params: {
|
|
989
|
+
name: toolName,
|
|
990
|
+
arguments: args
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
const response = await fetch(MCP_HTTP_ENDPOINT, {
|
|
994
|
+
method: "POST",
|
|
995
|
+
headers: {
|
|
996
|
+
"Content-Type": "application/json",
|
|
997
|
+
Accept: "application/json, text/event-stream",
|
|
998
|
+
Authorization: `Bearer ${accessToken}`,
|
|
999
|
+
"Mcp-Session-Id": sessionId
|
|
1000
|
+
},
|
|
1001
|
+
body: JSON.stringify(jsonRpcRequest)
|
|
1002
|
+
});
|
|
1003
|
+
if (!response.ok) {
|
|
1004
|
+
if (response.status === 400 || response.status === 401) {
|
|
1005
|
+
sessionCache.delete(accessToken);
|
|
1006
|
+
const newSessionId = await initializeMCPSession(accessToken);
|
|
1007
|
+
const retryResponse = await fetch(MCP_HTTP_ENDPOINT, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: {
|
|
1010
|
+
"Content-Type": "application/json",
|
|
1011
|
+
Accept: "application/json, text/event-stream",
|
|
1012
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1013
|
+
"Mcp-Session-Id": newSessionId
|
|
1014
|
+
},
|
|
1015
|
+
body: JSON.stringify(jsonRpcRequest)
|
|
1016
|
+
});
|
|
1017
|
+
if (!retryResponse.ok) {
|
|
1018
|
+
const errorBody2 = await retryResponse.text().catch(() => "");
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`MCP request failed: ${retryResponse.status} ${retryResponse.statusText}${errorBody2 ? ` - ${errorBody2}` : ""}`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const retryJsonRpcResponse = await parseResponseBody(retryResponse);
|
|
1024
|
+
if (retryJsonRpcResponse.error) {
|
|
1025
|
+
const err = retryJsonRpcResponse.error;
|
|
1026
|
+
throw new Error(`MCP tool error: ${err.message || JSON.stringify(err)}`);
|
|
1027
|
+
}
|
|
1028
|
+
return retryJsonRpcResponse.result;
|
|
1029
|
+
}
|
|
1030
|
+
const errorBody = await response.text().catch(() => "");
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`MCP request failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
const jsonRpcResponse = await parseResponseBody(response);
|
|
1036
|
+
if (jsonRpcResponse.error) {
|
|
1037
|
+
const err = jsonRpcResponse.error;
|
|
1038
|
+
throw new Error(`MCP tool error: ${err.message || JSON.stringify(err)}`);
|
|
1039
|
+
}
|
|
1040
|
+
return jsonRpcResponse.result;
|
|
1041
|
+
}
|
|
1042
|
+
function createNotionSearchTool(getAccessToken, requestNotionAccess) {
|
|
1043
|
+
return {
|
|
1044
|
+
type: "function",
|
|
1045
|
+
function: {
|
|
1046
|
+
name: "notion-search",
|
|
1047
|
+
description: "Semantic search over Notion workspace and connected sources (Slack, Google Drive, Github, Jira, etc). Can also search for users by name or email.",
|
|
1048
|
+
arguments: {
|
|
1049
|
+
type: "object",
|
|
1050
|
+
properties: {
|
|
1051
|
+
query: {
|
|
1052
|
+
type: "string",
|
|
1053
|
+
description: "Semantic search query over workspace and connected sources. For user search, a substring to match against name or email."
|
|
1054
|
+
},
|
|
1055
|
+
query_type: {
|
|
1056
|
+
type: "string",
|
|
1057
|
+
enum: ["internal", "user"],
|
|
1058
|
+
description: "Type of search: 'internal' for workspace/content search, 'user' for user search"
|
|
1059
|
+
},
|
|
1060
|
+
content_search_mode: {
|
|
1061
|
+
type: "string",
|
|
1062
|
+
enum: ["workspace_search", "ai_search"],
|
|
1063
|
+
description: "Override auto-selection of AI search vs workspace search"
|
|
1064
|
+
},
|
|
1065
|
+
data_source_url: {
|
|
1066
|
+
type: "string",
|
|
1067
|
+
description: "URL of a data source to search within"
|
|
1068
|
+
},
|
|
1069
|
+
page_url: {
|
|
1070
|
+
type: "string",
|
|
1071
|
+
description: "URL or ID of a page to search within"
|
|
1072
|
+
},
|
|
1073
|
+
teamspace_id: {
|
|
1074
|
+
type: "string",
|
|
1075
|
+
description: "ID of a teamspace to restrict search results to"
|
|
1076
|
+
},
|
|
1077
|
+
filters: {
|
|
1078
|
+
type: "object",
|
|
1079
|
+
description: "Filters for search results (only for query_type 'internal'). Supports created_date_range {start_date, end_date} and created_by_user_ids array."
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
required: ["query"]
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
executor: async (args) => {
|
|
1086
|
+
let token = getAccessToken();
|
|
1087
|
+
if (!token) {
|
|
1088
|
+
token = await requestNotionAccess();
|
|
1089
|
+
}
|
|
1090
|
+
try {
|
|
1091
|
+
return await callMCPTool(token, "notion-search", args);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
return `Error searching Notion: ${error instanceof Error ? error.message : String(error)}`;
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
autoExecute: true
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function createNotionFetchTool(getAccessToken, requestNotionAccess) {
|
|
1100
|
+
return {
|
|
1101
|
+
type: "function",
|
|
1102
|
+
function: {
|
|
1103
|
+
name: "notion-fetch",
|
|
1104
|
+
description: "Retrieve a Notion page or database by URL or ID. Pages return enhanced Markdown. Databases return all data sources.",
|
|
1105
|
+
arguments: {
|
|
1106
|
+
type: "object",
|
|
1107
|
+
properties: {
|
|
1108
|
+
id: {
|
|
1109
|
+
type: "string",
|
|
1110
|
+
description: "The ID or URL of the Notion page/database. Supports notion.so URLs, Notion Sites URLs (*.notion.site), and raw UUIDs."
|
|
1111
|
+
},
|
|
1112
|
+
include_transcript: {
|
|
1113
|
+
type: "boolean",
|
|
1114
|
+
description: "Include transcript if available"
|
|
1115
|
+
},
|
|
1116
|
+
include_discussions: {
|
|
1117
|
+
type: "boolean",
|
|
1118
|
+
description: "Include discussion counts and inline discussion markers"
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
required: ["id"]
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
executor: async (args) => {
|
|
1125
|
+
let token = getAccessToken();
|
|
1126
|
+
if (!token) {
|
|
1127
|
+
token = await requestNotionAccess();
|
|
1128
|
+
}
|
|
1129
|
+
try {
|
|
1130
|
+
return await callMCPTool(token, "notion-fetch", args);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
return `Error fetching Notion page: ${error instanceof Error ? error.message : String(error)}`;
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
autoExecute: true
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function createNotionCreatePagesTool(getAccessToken, requestNotionAccess) {
|
|
1139
|
+
return {
|
|
1140
|
+
type: "function",
|
|
1141
|
+
function: {
|
|
1142
|
+
name: "notion-create-pages",
|
|
1143
|
+
description: "Create one or more Notion pages. All pages in a single call share the same parent. Properties use simplified format: title is a string, dates use date:{prop}:start format, checkboxes use __YES__/__NO__. Content is Notion-flavored Markdown.",
|
|
1144
|
+
arguments: {
|
|
1145
|
+
type: "object",
|
|
1146
|
+
properties: {
|
|
1147
|
+
pages: {
|
|
1148
|
+
type: "array",
|
|
1149
|
+
description: "Array of page objects to create (max 100)",
|
|
1150
|
+
items: {
|
|
1151
|
+
type: "object",
|
|
1152
|
+
properties: {
|
|
1153
|
+
properties: {
|
|
1154
|
+
type: "object",
|
|
1155
|
+
description: "JSON map of property names to values. Title is a string, numbers are JS numbers, checkboxes use __YES__/__NO__, dates use date:{prop}:start/end/is_datetime format."
|
|
1156
|
+
},
|
|
1157
|
+
content: {
|
|
1158
|
+
type: "string",
|
|
1159
|
+
description: "Page content in Notion-flavored Markdown format"
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
required: ["properties", "content"]
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
parent: {
|
|
1166
|
+
type: "object",
|
|
1167
|
+
description: "Parent for all pages. Use {type:'page_id', page_id:'...'}, {type:'database_id', database_id:'...'}, or {type:'data_source_id', data_source_id:'...'}. Do NOT pass this field to create standalone private pages."
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
required: ["pages"]
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
executor: async (args) => {
|
|
1174
|
+
let token = getAccessToken();
|
|
1175
|
+
if (!token) {
|
|
1176
|
+
token = await requestNotionAccess();
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
return await callMCPTool(token, "notion-create-pages", args);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
return `Error creating Notion page: ${error instanceof Error ? error.message : String(error)}`;
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
autoExecute: true
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function createNotionUpdatePageTool(getAccessToken, requestNotionAccess) {
|
|
1188
|
+
return {
|
|
1189
|
+
type: "function",
|
|
1190
|
+
function: {
|
|
1191
|
+
name: "notion-update-page",
|
|
1192
|
+
description: "Update a Notion page's properties or content. Uses command-based operations: 'update_properties' to change properties, 'replace_content' to replace all content, 'replace_content_range' to replace specific text, 'insert_content_after' to insert after text.",
|
|
1193
|
+
arguments: {
|
|
1194
|
+
type: "object",
|
|
1195
|
+
properties: {
|
|
1196
|
+
data: {
|
|
1197
|
+
type: "object",
|
|
1198
|
+
description: "Update data containing page_id and a command",
|
|
1199
|
+
properties: {
|
|
1200
|
+
page_id: {
|
|
1201
|
+
type: "string",
|
|
1202
|
+
description: "The ID of the page to update (with or without dashes)"
|
|
1203
|
+
},
|
|
1204
|
+
command: {
|
|
1205
|
+
type: "string",
|
|
1206
|
+
enum: [
|
|
1207
|
+
"update_properties",
|
|
1208
|
+
"replace_content",
|
|
1209
|
+
"replace_content_range",
|
|
1210
|
+
"insert_content_after"
|
|
1211
|
+
],
|
|
1212
|
+
description: "The update command to execute"
|
|
1213
|
+
},
|
|
1214
|
+
properties: {
|
|
1215
|
+
type: "object",
|
|
1216
|
+
description: "For update_properties: JSON map of property names to values. Use null to remove a value."
|
|
1217
|
+
},
|
|
1218
|
+
new_str: {
|
|
1219
|
+
type: "string",
|
|
1220
|
+
description: "For replace_content/replace_content_range/insert_content_after: the new content string"
|
|
1221
|
+
},
|
|
1222
|
+
selection_with_ellipsis: {
|
|
1223
|
+
type: "string",
|
|
1224
|
+
description: "For replace_content_range/insert_content_after: unique start and end snippet (~10 chars each with ellipsis)"
|
|
1225
|
+
},
|
|
1226
|
+
allow_deleting_content: {
|
|
1227
|
+
type: "boolean",
|
|
1228
|
+
description: "For replace_content/replace_content_range: allow deletion of child pages/databases"
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
required: ["page_id", "command"]
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
required: ["data"]
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
executor: async (args) => {
|
|
1238
|
+
let token = getAccessToken();
|
|
1239
|
+
if (!token) {
|
|
1240
|
+
token = await requestNotionAccess();
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
return await callMCPTool(token, "notion-update-page", args);
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
return `Error updating Notion page: ${error instanceof Error ? error.message : String(error)}`;
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
autoExecute: true
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function createNotionMovePagesTool(getAccessToken, requestNotionAccess) {
|
|
1252
|
+
return {
|
|
1253
|
+
type: "function",
|
|
1254
|
+
function: {
|
|
1255
|
+
name: "notion-move-pages",
|
|
1256
|
+
description: "Move one or more Notion pages or databases to a new parent.",
|
|
1257
|
+
arguments: {
|
|
1258
|
+
type: "object",
|
|
1259
|
+
properties: {
|
|
1260
|
+
page_or_database_ids: {
|
|
1261
|
+
type: "array",
|
|
1262
|
+
description: "Array of page or database IDs to move (UUIDs, max 100)",
|
|
1263
|
+
items: { type: "string" }
|
|
1264
|
+
},
|
|
1265
|
+
new_parent: {
|
|
1266
|
+
type: "object",
|
|
1267
|
+
description: "New parent: {type:'page_id', page_id:'...'}, {type:'database_id', database_id:'...'}, {type:'data_source_id', data_source_id:'...'}, or {type:'workspace'} for private pages."
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
required: ["page_or_database_ids", "new_parent"]
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
executor: async (args) => {
|
|
1274
|
+
let token = getAccessToken();
|
|
1275
|
+
if (!token) {
|
|
1276
|
+
token = await requestNotionAccess();
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
return await callMCPTool(token, "notion-move-pages", args);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return `Error moving Notion pages: ${error instanceof Error ? error.message : String(error)}`;
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
autoExecute: true
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function createNotionDuplicatePageTool(getAccessToken, requestNotionAccess) {
|
|
1288
|
+
return {
|
|
1289
|
+
type: "function",
|
|
1290
|
+
function: {
|
|
1291
|
+
name: "notion-duplicate-page",
|
|
1292
|
+
description: "Duplicate a Notion page. The duplication completes asynchronously.",
|
|
1293
|
+
arguments: {
|
|
1294
|
+
type: "object",
|
|
1295
|
+
properties: {
|
|
1296
|
+
page_id: {
|
|
1297
|
+
type: "string",
|
|
1298
|
+
description: "The ID of the page to duplicate (UUID with or without dashes)"
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
required: ["page_id"]
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
executor: async (args) => {
|
|
1305
|
+
let token = getAccessToken();
|
|
1306
|
+
if (!token) {
|
|
1307
|
+
token = await requestNotionAccess();
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
return await callMCPTool(token, "notion-duplicate-page", args);
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
return `Error duplicating Notion page: ${error instanceof Error ? error.message : String(error)}`;
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
autoExecute: true
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess) {
|
|
1319
|
+
return {
|
|
1320
|
+
type: "function",
|
|
1321
|
+
function: {
|
|
1322
|
+
name: "notion-create-database",
|
|
1323
|
+
description: "Create a new Notion database with a properties schema. If no title property is provided, 'Name' is auto-added. Supports property types: title, rich_text, number, select, multi_select, date, people, checkbox, url, email, phone_number, formula, relation, rollup, status, unique_id, etc.",
|
|
1324
|
+
arguments: {
|
|
1325
|
+
type: "object",
|
|
1326
|
+
properties: {
|
|
1327
|
+
properties: {
|
|
1328
|
+
type: "object",
|
|
1329
|
+
description: "Property schema for the database. Each key is a property name, value defines the type."
|
|
1330
|
+
},
|
|
1331
|
+
parent: {
|
|
1332
|
+
type: "object",
|
|
1333
|
+
description: "Parent page: {type:'page_id', page_id:'...'}. Omit for private workspace-level database."
|
|
1334
|
+
},
|
|
1335
|
+
title: {
|
|
1336
|
+
type: "array",
|
|
1337
|
+
description: "Title of the database as rich text array (max 100)",
|
|
1338
|
+
items: { type: "object" }
|
|
1339
|
+
},
|
|
1340
|
+
description: {
|
|
1341
|
+
type: "array",
|
|
1342
|
+
description: "Description of the database as rich text array (max 100)",
|
|
1343
|
+
items: { type: "object" }
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
required: ["properties"]
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1349
|
+
executor: async (args) => {
|
|
1350
|
+
let token = getAccessToken();
|
|
1351
|
+
if (!token) {
|
|
1352
|
+
token = await requestNotionAccess();
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
return await callMCPTool(token, "notion-create-database", args);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
return `Error creating Notion database: ${error instanceof Error ? error.message : String(error)}`;
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
autoExecute: true
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess) {
|
|
1364
|
+
return {
|
|
1365
|
+
type: "function",
|
|
1366
|
+
function: {
|
|
1367
|
+
name: "notion-update-data-source",
|
|
1368
|
+
description: "Update a Notion data source's title, description, property schema, or other attributes. Use null to remove a property. Provide only 'name' to rename a property.",
|
|
1369
|
+
arguments: {
|
|
1370
|
+
type: "object",
|
|
1371
|
+
properties: {
|
|
1372
|
+
data_source_id: {
|
|
1373
|
+
type: "string",
|
|
1374
|
+
description: "The ID of the data source to update (UUID). Can be a data source ID or database ID."
|
|
1375
|
+
},
|
|
1376
|
+
title: {
|
|
1377
|
+
type: "array",
|
|
1378
|
+
description: "New title as rich text array (max 100)",
|
|
1379
|
+
items: { type: "object" }
|
|
1380
|
+
},
|
|
1381
|
+
description: {
|
|
1382
|
+
type: "array",
|
|
1383
|
+
description: "New description as rich text array (max 100)",
|
|
1384
|
+
items: { type: "object" }
|
|
1385
|
+
},
|
|
1386
|
+
properties: {
|
|
1387
|
+
type: "object",
|
|
1388
|
+
description: "Property schema updates. Use null to remove, {name:'...'} to rename, or full definition to add/update."
|
|
1389
|
+
},
|
|
1390
|
+
is_inline: {
|
|
1391
|
+
type: "boolean",
|
|
1392
|
+
description: "Whether the database is inline"
|
|
1393
|
+
},
|
|
1394
|
+
in_trash: {
|
|
1395
|
+
type: "boolean",
|
|
1396
|
+
description: "Trash the data source (requires Notion UI to undo)"
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
required: ["data_source_id"]
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
executor: async (args) => {
|
|
1403
|
+
let token = getAccessToken();
|
|
1404
|
+
if (!token) {
|
|
1405
|
+
token = await requestNotionAccess();
|
|
1406
|
+
}
|
|
1407
|
+
try {
|
|
1408
|
+
return await callMCPTool(token, "notion-update-data-source", args);
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
return `Error updating Notion data source: ${error instanceof Error ? error.message : String(error)}`;
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
autoExecute: true
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function createNotionCreateCommentTool(getAccessToken, requestNotionAccess) {
|
|
1417
|
+
return {
|
|
1418
|
+
type: "function",
|
|
1419
|
+
function: {
|
|
1420
|
+
name: "notion-create-comment",
|
|
1421
|
+
description: "Add a comment to a Notion page. Supports page-level comments, content-specific comments (using selection_with_ellipsis), or replies to existing discussions (using discussion_id).",
|
|
1422
|
+
arguments: {
|
|
1423
|
+
type: "object",
|
|
1424
|
+
properties: {
|
|
1425
|
+
page_id: {
|
|
1426
|
+
type: "string",
|
|
1427
|
+
description: "The ID of the page to comment on (with or without dashes)"
|
|
1428
|
+
},
|
|
1429
|
+
rich_text: {
|
|
1430
|
+
type: "array",
|
|
1431
|
+
description: "Comment content as rich text array (max 100). Objects can be text, mention, or equation.",
|
|
1432
|
+
items: {
|
|
1433
|
+
type: "object",
|
|
1434
|
+
properties: {
|
|
1435
|
+
text: {
|
|
1436
|
+
type: "object",
|
|
1437
|
+
properties: {
|
|
1438
|
+
content: { type: "string" }
|
|
1439
|
+
},
|
|
1440
|
+
required: ["content"]
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
required: ["text"]
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
discussion_id: {
|
|
1447
|
+
type: "string",
|
|
1448
|
+
description: "ID or URL of an existing discussion to reply to"
|
|
1449
|
+
},
|
|
1450
|
+
selection_with_ellipsis: {
|
|
1451
|
+
type: "string",
|
|
1452
|
+
description: "Unique start and end snippet (~10 chars each with ellipsis) of content to comment on"
|
|
1453
|
+
}
|
|
1454
|
+
},
|
|
1455
|
+
required: ["page_id", "rich_text"]
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
executor: async (args) => {
|
|
1459
|
+
let token = getAccessToken();
|
|
1460
|
+
if (!token) {
|
|
1461
|
+
token = await requestNotionAccess();
|
|
1462
|
+
}
|
|
1463
|
+
try {
|
|
1464
|
+
return await callMCPTool(token, "notion-create-comment", args);
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
return `Error creating Notion comment: ${error instanceof Error ? error.message : String(error)}`;
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
autoExecute: true
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function createNotionGetCommentsTool(getAccessToken, requestNotionAccess) {
|
|
1473
|
+
return {
|
|
1474
|
+
type: "function",
|
|
1475
|
+
function: {
|
|
1476
|
+
name: "notion-get-comments",
|
|
1477
|
+
description: "Get comments and discussions from a Notion page. Returns discussions with full comment content.",
|
|
1478
|
+
arguments: {
|
|
1479
|
+
type: "object",
|
|
1480
|
+
properties: {
|
|
1481
|
+
page_id: {
|
|
1482
|
+
type: "string",
|
|
1483
|
+
description: "Identifier for the Notion page"
|
|
1484
|
+
},
|
|
1485
|
+
include_all_blocks: {
|
|
1486
|
+
type: "boolean",
|
|
1487
|
+
description: "Include discussions on child blocks (default: false)"
|
|
1488
|
+
},
|
|
1489
|
+
include_resolved: {
|
|
1490
|
+
type: "boolean",
|
|
1491
|
+
description: "Include resolved discussions (default: false)"
|
|
1492
|
+
},
|
|
1493
|
+
discussion_id: {
|
|
1494
|
+
type: "string",
|
|
1495
|
+
description: "Fetch a specific discussion by ID or URL (e.g., discussion://pageId/blockId/discussionId)"
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
required: ["page_id"]
|
|
1499
|
+
}
|
|
1500
|
+
},
|
|
1501
|
+
executor: async (args) => {
|
|
1502
|
+
let token = getAccessToken();
|
|
1503
|
+
if (!token) {
|
|
1504
|
+
token = await requestNotionAccess();
|
|
1505
|
+
}
|
|
1506
|
+
try {
|
|
1507
|
+
return await callMCPTool(token, "notion-get-comments", args);
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
return `Error retrieving Notion comments: ${error instanceof Error ? error.message : String(error)}`;
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
autoExecute: true
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function createNotionGetUsersTool(getAccessToken, requestNotionAccess) {
|
|
1516
|
+
return {
|
|
1517
|
+
type: "function",
|
|
1518
|
+
function: {
|
|
1519
|
+
name: "notion-get-users",
|
|
1520
|
+
description: "List users in the Notion workspace. Supports filtering by name/email, pagination, fetching a specific user by ID, or fetching the current bot user (user_id: 'self').",
|
|
1521
|
+
arguments: {
|
|
1522
|
+
type: "object",
|
|
1523
|
+
properties: {
|
|
1524
|
+
query: {
|
|
1525
|
+
type: "string",
|
|
1526
|
+
description: "Search query to filter users by name or email (case-insensitive, max 100 chars)"
|
|
1527
|
+
},
|
|
1528
|
+
start_cursor: {
|
|
1529
|
+
type: "string",
|
|
1530
|
+
description: "Cursor for pagination from previous response"
|
|
1531
|
+
},
|
|
1532
|
+
page_size: {
|
|
1533
|
+
type: "integer",
|
|
1534
|
+
description: "Number of users per page (1-100, default 100)"
|
|
1535
|
+
},
|
|
1536
|
+
user_id: {
|
|
1537
|
+
type: "string",
|
|
1538
|
+
description: "Return only the user matching this ID. Pass 'self' to fetch the current bot user."
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
executor: async (args) => {
|
|
1544
|
+
let token = getAccessToken();
|
|
1545
|
+
if (!token) {
|
|
1546
|
+
token = await requestNotionAccess();
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
return await callMCPTool(token, "notion-get-users", args);
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
return `Error listing Notion users: ${error instanceof Error ? error.message : String(error)}`;
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
autoExecute: true
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function createNotionGetTeamsTool(getAccessToken, requestNotionAccess) {
|
|
1558
|
+
return {
|
|
1559
|
+
type: "function",
|
|
1560
|
+
function: {
|
|
1561
|
+
name: "notion-get-teams",
|
|
1562
|
+
description: "Retrieve teams (teamspaces) in the Notion workspace. Shows team names, IDs, membership status, and roles.",
|
|
1563
|
+
arguments: {
|
|
1564
|
+
type: "object",
|
|
1565
|
+
properties: {
|
|
1566
|
+
query: {
|
|
1567
|
+
type: "string",
|
|
1568
|
+
description: "Search query to filter teams by name (case-insensitive, max 100 chars)"
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
executor: async (args) => {
|
|
1574
|
+
let token = getAccessToken();
|
|
1575
|
+
if (!token) {
|
|
1576
|
+
token = await requestNotionAccess();
|
|
1577
|
+
}
|
|
1578
|
+
try {
|
|
1579
|
+
return await callMCPTool(token, "notion-get-teams", args);
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
return `Error retrieving Notion teams: ${error instanceof Error ? error.message : String(error)}`;
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
autoExecute: true
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function createNotionTools(getAccessToken, requestNotionAccess) {
|
|
1588
|
+
return [
|
|
1589
|
+
// Search
|
|
1590
|
+
createNotionSearchTool(getAccessToken, requestNotionAccess),
|
|
1591
|
+
// Pages
|
|
1592
|
+
createNotionFetchTool(getAccessToken, requestNotionAccess),
|
|
1593
|
+
createNotionCreatePagesTool(getAccessToken, requestNotionAccess),
|
|
1594
|
+
createNotionUpdatePageTool(getAccessToken, requestNotionAccess),
|
|
1595
|
+
createNotionMovePagesTool(getAccessToken, requestNotionAccess),
|
|
1596
|
+
createNotionDuplicatePageTool(getAccessToken, requestNotionAccess),
|
|
1597
|
+
// Data Sources (Databases)
|
|
1598
|
+
createNotionCreateDatabaseTool(getAccessToken, requestNotionAccess),
|
|
1599
|
+
createNotionUpdateDataSourceTool(getAccessToken, requestNotionAccess),
|
|
1600
|
+
// Comments
|
|
1601
|
+
createNotionCreateCommentTool(getAccessToken, requestNotionAccess),
|
|
1602
|
+
createNotionGetCommentsTool(getAccessToken, requestNotionAccess),
|
|
1603
|
+
// Users & Teams
|
|
1604
|
+
createNotionGetUsersTool(getAccessToken, requestNotionAccess),
|
|
1605
|
+
createNotionGetTeamsTool(getAccessToken, requestNotionAccess)
|
|
1606
|
+
];
|
|
1607
|
+
}
|
|
1608
|
+
function getMCPEndpoints() {
|
|
1609
|
+
return {
|
|
1610
|
+
http: MCP_HTTP_ENDPOINT,
|
|
1611
|
+
sse: MCP_SSE_ENDPOINT
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
async function callNotionMCPTool(accessToken, toolName, args) {
|
|
1615
|
+
return callMCPTool(accessToken, toolName, args);
|
|
1616
|
+
}
|
|
1617
|
+
export {
|
|
1618
|
+
callNotionMCPTool,
|
|
1619
|
+
createChartTool,
|
|
1620
|
+
createChatTools,
|
|
1621
|
+
createDisplayTool,
|
|
1622
|
+
createDriveTools,
|
|
1623
|
+
createGoogleCalendarCreateEventTool,
|
|
1624
|
+
createGoogleCalendarTool,
|
|
1625
|
+
createGoogleCalendarUpdateEventTool,
|
|
1626
|
+
createGoogleDriveGetContentTool,
|
|
1627
|
+
createGoogleDriveListRecentTool,
|
|
1628
|
+
createGoogleDriveSearchTool,
|
|
1629
|
+
createInteractiveTool,
|
|
1630
|
+
createNotionCreateCommentTool,
|
|
1631
|
+
createNotionCreateDatabaseTool,
|
|
1632
|
+
createNotionCreatePagesTool,
|
|
1633
|
+
createNotionDuplicatePageTool,
|
|
1634
|
+
createNotionFetchTool,
|
|
1635
|
+
createNotionGetCommentsTool,
|
|
1636
|
+
createNotionGetTeamsTool,
|
|
1637
|
+
createNotionGetUsersTool,
|
|
1638
|
+
createNotionMovePagesTool,
|
|
1639
|
+
createNotionSearchTool,
|
|
1640
|
+
createNotionTools,
|
|
1641
|
+
createNotionUpdateDataSourceTool,
|
|
1642
|
+
createNotionUpdatePageTool,
|
|
1643
|
+
getMCPEndpoints,
|
|
1644
|
+
migrateDisplayResult
|
|
1645
|
+
};
|