@attrove/mcp 0.1.6 → 0.1.8

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.
Files changed (47) hide show
  1. package/README.md +101 -3
  2. package/cjs/index.js +6 -1
  3. package/cjs/server.js +115 -56
  4. package/cjs/tools/events.js +134 -0
  5. package/cjs/tools/index.js +9 -1
  6. package/cjs/tools/integrations.js +11 -4
  7. package/cjs/tools/meetings.js +149 -0
  8. package/cjs/tools/query.js +12 -5
  9. package/cjs/tools/search.js +21 -7
  10. package/cjs/transport/http.js +192 -0
  11. package/esm/index.d.ts +3 -1
  12. package/esm/index.d.ts.map +1 -1
  13. package/esm/index.js +3 -1
  14. package/esm/index.js.map +1 -1
  15. package/esm/server.d.ts +7 -5
  16. package/esm/server.d.ts.map +1 -1
  17. package/esm/server.js +116 -57
  18. package/esm/server.js.map +1 -1
  19. package/esm/tools/events.d.ts +24 -0
  20. package/esm/tools/events.d.ts.map +1 -0
  21. package/esm/tools/events.js +131 -0
  22. package/esm/tools/events.js.map +1 -0
  23. package/esm/tools/index.d.ts +6 -69
  24. package/esm/tools/index.d.ts.map +1 -1
  25. package/esm/tools/index.js +5 -2
  26. package/esm/tools/index.js.map +1 -1
  27. package/esm/tools/integrations.d.ts +3 -10
  28. package/esm/tools/integrations.d.ts.map +1 -1
  29. package/esm/tools/integrations.js +11 -4
  30. package/esm/tools/integrations.js.map +1 -1
  31. package/esm/tools/meetings.d.ts +26 -0
  32. package/esm/tools/meetings.d.ts.map +1 -0
  33. package/esm/tools/meetings.js +146 -0
  34. package/esm/tools/meetings.js.map +1 -0
  35. package/esm/tools/query.d.ts +3 -27
  36. package/esm/tools/query.d.ts.map +1 -1
  37. package/esm/tools/query.js +12 -5
  38. package/esm/tools/query.js.map +1 -1
  39. package/esm/tools/search.d.ts +3 -35
  40. package/esm/tools/search.d.ts.map +1 -1
  41. package/esm/tools/search.js +21 -7
  42. package/esm/tools/search.js.map +1 -1
  43. package/esm/transport/http.d.ts +63 -0
  44. package/esm/transport/http.d.ts.map +1 -0
  45. package/esm/transport/http.js +189 -0
  46. package/esm/transport/http.js.map +1 -0
  47. package/package.json +2 -2
package/README.md CHANGED
@@ -66,6 +66,33 @@ export ATTROVE_API_KEY="sk_..."
66
66
  export ATTROVE_USER_ID="user-uuid"
67
67
  ```
68
68
 
69
+ ### ChatGPT (via HTTP endpoint)
70
+
71
+ ChatGPT and other AI assistants that support MCP connectors can connect via the hosted HTTP endpoint.
72
+
73
+ > **Note:** ChatGPT's connector interface may change over time. If these steps don't match your experience, refer to [OpenAI's current documentation](https://help.openai.com) for the latest instructions.
74
+
75
+ **Basic requirements:**
76
+ - HTTP endpoint URL: `https://api.attrove.com/mcp`
77
+ - Authentication: Bearer token (`sk_...`)
78
+ - Custom header: `X-Attrove-User-Id` with your user UUID
79
+
80
+ **Example setup steps (may vary):**
81
+ 1. Open ChatGPT Settings → **Connectors** → Enable **Developer Mode**
82
+ 2. Click **Create Connector** and configure:
83
+ - **Name:** `Attrove`
84
+ - **URL:** `https://api.attrove.com/mcp`
85
+ - **Authentication:** Bearer token
86
+ - **Token:** Your API key (`sk_...`)
87
+ 3. Add custom header:
88
+ - **Header:** `X-Attrove-User-Id`
89
+ - **Value:** Your user ID (UUID)
90
+
91
+ Once connected, you can ask ChatGPT questions like:
92
+ - "What emails need my attention this week?"
93
+ - "Summarize my meeting with the marketing team"
94
+ - "What has John been asking about lately?"
95
+
69
96
  ### Direct CLI Usage
70
97
 
71
98
  ```bash
@@ -99,8 +126,8 @@ Ask questions about the user's communications and get AI-generated answers.
99
126
 
100
127
  **Parameters:**
101
128
  - `query` (required): The question to ask
102
- - `integration_ids` (optional): Filter to specific integration IDs (UUIDs, e.g., `["550e8400-e29b-41d4-a716-446655440000"]`)
103
- - `include_sources` (optional): Include source snippets
129
+ - `integration_ids` (optional): Filter to specific integration IDs (array of UUID strings)
130
+ - `include_sources` (optional): Include source snippets in the response
104
131
 
105
132
  **Example prompts:**
106
133
  - "What did Sarah say about the Q4 budget?"
@@ -118,7 +145,7 @@ Search for specific messages or conversations.
118
145
  - `after_date` (optional): Only messages after this date (ISO 8601)
119
146
  - `before_date` (optional): Only messages before this date
120
147
  - `sender_domains` (optional): Filter by sender domains
121
- - `include_body_text` (optional): Include message content (default: true)
148
+ - `include_body_text` (optional): Include message content in results (default: true, bodies truncated to 200 characters)
122
149
 
123
150
  **Example prompts:**
124
151
  - "Find all emails about the product launch"
@@ -165,6 +192,77 @@ await startServer({
165
192
  });
166
193
  ```
167
194
 
195
+ ### HTTP Endpoint (Hosted)
196
+
197
+ For AI assistants that connect via HTTP (like ChatGPT), use the hosted endpoint:
198
+
199
+ ```bash
200
+ # Test the endpoint
201
+ curl -X POST https://api.attrove.com/mcp \
202
+ -H "Authorization: Bearer sk_..." \
203
+ -H "X-Attrove-User-Id: user-uuid" \
204
+ -H "Content-Type: application/json" \
205
+ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
206
+ ```
207
+
208
+ Or integrate in your own server using the HTTP handler:
209
+
210
+ ```typescript
211
+ import { createHttpHandler } from '@attrove/mcp';
212
+
213
+ const handler = createHttpHandler(
214
+ {
215
+ apiKey: 'sk_...',
216
+ userId: 'user-uuid',
217
+ baseUrl: 'https://api.attrove.com', // optional: custom API endpoint
218
+ },
219
+ {
220
+ enableJsonResponse: true, // optional: use JSON instead of SSE (default: true)
221
+ timeoutMs: 30000, // optional: request timeout in ms (default: 30000)
222
+ }
223
+ );
224
+
225
+ // With Fastify (recommended)
226
+ fastify.post('/mcp', async (request, reply) => {
227
+ const result = await handler.handleRequest(request.raw, reply.raw, request.body);
228
+
229
+ if (!result.handled) {
230
+ // Handle timeout with 504, other errors with 500
231
+ const statusCode = result.isTimeout ? 504 : 500;
232
+ const userMessage = result.isTimeout
233
+ ? 'Request timed out. Try a simpler query or reduce the scope.'
234
+ : 'An unexpected error occurred. Please try again.';
235
+
236
+ // Only send response if headers haven't been sent (e.g., during streaming)
237
+ if (!reply.raw.headersSent) {
238
+ reply.code(statusCode).send({
239
+ success: false,
240
+ error: { code: result.isTimeout ? 'REQUEST_TIMEOUT' : 'INTERNAL_ERROR', message: userMessage }
241
+ });
242
+ } else if (!reply.raw.writableEnded) {
243
+ reply.raw.end(); // Ensure stream is closed
244
+ }
245
+ return;
246
+ }
247
+
248
+ // Optional: monitor cleanup failures for resource leak detection
249
+ if (result.cleanupFailed) {
250
+ console.warn('MCP cleanup failed - potential resource leak');
251
+ }
252
+ });
253
+
254
+ // With raw Node.js HTTP server
255
+ import { createServer } from 'node:http';
256
+ const server = createServer(async (req, res) => {
257
+ // Note: You'll need to parse the body yourself for raw HTTP
258
+ const result = await handler.handleRequest(req, res);
259
+ if (!result.handled) {
260
+ res.writeHead(500, { 'Content-Type': 'application/json' });
261
+ res.end(JSON.stringify({ error: result.error }));
262
+ }
263
+ });
264
+ ```
265
+
168
266
  ## Getting API Credentials
169
267
 
170
268
  1. Sign up at [attrove.com](https://attrove.com)
package/cjs/index.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * @packageDocumentation
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.getVersion = exports.integrationsToolDefinition = exports.searchToolDefinition = exports.queryToolDefinition = exports.allToolDefinitions = exports.getConfigFromEnv = exports.startServer = exports.createServer = void 0;
31
+ exports.createHttpHandler = exports.getVersion = exports.meetingsToolDefinition = exports.eventsToolDefinition = exports.integrationsToolDefinition = exports.searchToolDefinition = exports.queryToolDefinition = exports.allToolDefinitions = exports.getConfigFromEnv = exports.startServer = exports.createServer = void 0;
32
32
  var server_js_1 = require("./server.js");
33
33
  Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_js_1.createServer; } });
34
34
  Object.defineProperty(exports, "startServer", { enumerable: true, get: function () { return server_js_1.startServer; } });
@@ -38,5 +38,10 @@ Object.defineProperty(exports, "allToolDefinitions", { enumerable: true, get: fu
38
38
  Object.defineProperty(exports, "queryToolDefinition", { enumerable: true, get: function () { return index_js_1.queryToolDefinition; } });
39
39
  Object.defineProperty(exports, "searchToolDefinition", { enumerable: true, get: function () { return index_js_1.searchToolDefinition; } });
40
40
  Object.defineProperty(exports, "integrationsToolDefinition", { enumerable: true, get: function () { return index_js_1.integrationsToolDefinition; } });
41
+ Object.defineProperty(exports, "eventsToolDefinition", { enumerable: true, get: function () { return index_js_1.eventsToolDefinition; } });
42
+ Object.defineProperty(exports, "meetingsToolDefinition", { enumerable: true, get: function () { return index_js_1.meetingsToolDefinition; } });
41
43
  var version_js_1 = require("./version.js");
42
44
  Object.defineProperty(exports, "getVersion", { enumerable: true, get: function () { return version_js_1.getVersion; } });
45
+ // HTTP transport for hosted MCP endpoints
46
+ var http_js_1 = require("./transport/http.js");
47
+ Object.defineProperty(exports, "createHttpHandler", { enumerable: true, get: function () { return http_js_1.createHttpHandler; } });
package/cjs/server.js CHANGED
@@ -44,12 +44,13 @@ function validateQueryInput(args) {
44
44
  if (typeof input.query !== "string" || input.query.trim() === "") {
45
45
  throw new sdk_1.ValidationError('Missing required parameter "query". Please provide a question to ask.', sdk_1.ErrorCodes.VALIDATION_REQUIRED_FIELD, { field: "query" });
46
46
  }
47
+ if (input.include_sources !== undefined && typeof input.include_sources !== "boolean") {
48
+ throw new sdk_1.ValidationError(`include_sources must be a boolean, received ${typeof input.include_sources}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "include_sources", received: typeof input.include_sources });
49
+ }
47
50
  return {
48
51
  query: input.query,
49
52
  integration_ids: validateStringArray(input.integration_ids, "integration_ids"),
50
- include_sources: typeof input.include_sources === "boolean"
51
- ? input.include_sources
52
- : undefined,
53
+ include_sources: input.include_sources,
53
54
  };
54
55
  }
55
56
  /**
@@ -65,14 +66,79 @@ function validateSearchInput(args) {
65
66
  if (typeof input.query !== "string" || input.query.trim() === "") {
66
67
  throw new sdk_1.ValidationError('Missing required parameter "query". Please provide a search query.', sdk_1.ErrorCodes.VALIDATION_REQUIRED_FIELD, { field: "query" });
67
68
  }
69
+ if (input.after_date !== undefined && typeof input.after_date !== "string") {
70
+ throw new sdk_1.ValidationError(`after_date must be a string (ISO 8601), received ${typeof input.after_date}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "after_date", received: typeof input.after_date });
71
+ }
72
+ if (input.before_date !== undefined && typeof input.before_date !== "string") {
73
+ throw new sdk_1.ValidationError(`before_date must be a string (ISO 8601), received ${typeof input.before_date}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "before_date", received: typeof input.before_date });
74
+ }
75
+ if (input.include_body_text !== undefined && typeof input.include_body_text !== "boolean") {
76
+ throw new sdk_1.ValidationError(`include_body_text must be a boolean, received ${typeof input.include_body_text}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "include_body_text", received: typeof input.include_body_text });
77
+ }
68
78
  return {
69
79
  query: input.query,
70
- after_date: typeof input.after_date === "string" ? input.after_date : undefined,
71
- before_date: typeof input.before_date === "string" ? input.before_date : undefined,
80
+ after_date: input.after_date,
81
+ before_date: input.before_date,
72
82
  sender_domains: validateStringArray(input.sender_domains, "sender_domains"),
73
- include_body_text: typeof input.include_body_text === "boolean"
74
- ? input.include_body_text
75
- : undefined,
83
+ include_body_text: input.include_body_text,
84
+ };
85
+ }
86
+ /**
87
+ * Validate input for the events tool.
88
+ * All parameters are optional, so validation is light type checking.
89
+ */
90
+ function validateEventsInput(args) {
91
+ if (args !== undefined && args !== null && typeof args !== "object") {
92
+ throw new sdk_1.ValidationError("Invalid arguments. Expected an object or empty.", sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { expected: "object", received: typeof args });
93
+ }
94
+ const input = (args ?? {});
95
+ if (input.start_date !== undefined && typeof input.start_date !== "string") {
96
+ throw new sdk_1.ValidationError(`start_date must be a string (ISO 8601), received ${typeof input.start_date}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "start_date", received: typeof input.start_date });
97
+ }
98
+ if (input.end_date !== undefined && typeof input.end_date !== "string") {
99
+ throw new sdk_1.ValidationError(`end_date must be a string (ISO 8601), received ${typeof input.end_date}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "end_date", received: typeof input.end_date });
100
+ }
101
+ if (input.limit !== undefined && typeof input.limit !== "number") {
102
+ throw new sdk_1.ValidationError(`limit must be a number, received ${typeof input.limit}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "limit", received: typeof input.limit });
103
+ }
104
+ return {
105
+ start_date: input.start_date,
106
+ end_date: input.end_date,
107
+ limit: input.limit,
108
+ };
109
+ }
110
+ /**
111
+ * Validate input for the meetings tool.
112
+ * All parameters are optional, so validation is light type checking.
113
+ */
114
+ function validateMeetingsInput(args) {
115
+ if (args !== undefined && args !== null && typeof args !== "object") {
116
+ throw new sdk_1.ValidationError("Invalid arguments. Expected an object or empty.", sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { expected: "object", received: typeof args });
117
+ }
118
+ const input = (args ?? {});
119
+ const validProviders = ["google_meet", "zoom", "teams"];
120
+ let provider;
121
+ if (input.provider !== undefined && input.provider !== null) {
122
+ if (typeof input.provider !== "string" ||
123
+ !validProviders.includes(input.provider)) {
124
+ throw new sdk_1.ValidationError(`Invalid provider "${String(input.provider)}". Must be one of: ${validProviders.join(", ")}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "provider", received: input.provider, valid: validProviders });
125
+ }
126
+ provider = input.provider;
127
+ }
128
+ if (input.start_date !== undefined && typeof input.start_date !== "string") {
129
+ throw new sdk_1.ValidationError(`start_date must be a string (ISO 8601), received ${typeof input.start_date}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "start_date", received: typeof input.start_date });
130
+ }
131
+ if (input.end_date !== undefined && typeof input.end_date !== "string") {
132
+ throw new sdk_1.ValidationError(`end_date must be a string (ISO 8601), received ${typeof input.end_date}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "end_date", received: typeof input.end_date });
133
+ }
134
+ if (input.limit !== undefined && typeof input.limit !== "number") {
135
+ throw new sdk_1.ValidationError(`limit must be a number, received ${typeof input.limit}`, sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { field: "limit", received: typeof input.limit });
136
+ }
137
+ return {
138
+ start_date: input.start_date,
139
+ end_date: input.end_date,
140
+ provider,
141
+ limit: input.limit,
76
142
  };
77
143
  }
78
144
  /**
@@ -121,7 +187,7 @@ function formatErrorResponse(error) {
121
187
  };
122
188
  }
123
189
  /**
124
- * Create and configure the Attrove MCP server.
190
+ * Wire up tool definitions and dispatch logic for the Attrove MCP server.
125
191
  */
126
192
  function createServer(config) {
127
193
  const server = new index_js_1.Server({
@@ -132,85 +198,75 @@ function createServer(config) {
132
198
  tools: {},
133
199
  },
134
200
  });
135
- // Create Attrove client
136
- // Type assertion is safe because startServer validates the apiKey prefix
137
201
  const client = new sdk_1.Attrove({
138
202
  apiKey: config.apiKey,
139
203
  userId: config.userId,
140
204
  baseUrl: config.baseUrl,
141
205
  });
142
- // Register tool list handler
143
206
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
144
207
  return {
145
208
  tools: index_js_2.allToolDefinitions.map((tool) => ({
146
209
  name: tool.name,
147
210
  description: tool.description,
148
211
  inputSchema: tool.inputSchema,
212
+ annotations: tool.annotations,
149
213
  })),
150
214
  };
151
215
  });
152
- // Register tool call handler
153
216
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
154
217
  const { name, arguments: args } = request.params;
155
- // Validate input outside try-catch so validation errors propagate clearly
156
- // and programming bugs in validation don't get caught as API errors
157
- let validatedInput;
158
- switch (name) {
159
- case "attrove_query":
160
- validatedInput = validateQueryInput(args);
161
- break;
162
- case "attrove_search":
163
- validatedInput = validateSearchInput(args);
164
- break;
165
- case "attrove_integrations":
166
- // No input validation needed
167
- break;
168
- default:
169
- return {
170
- content: [
171
- {
172
- type: "text",
173
- text: `Unknown tool: ${name}. Available tools: attrove_query, attrove_search, attrove_integrations`,
174
- },
175
- ],
176
- isError: true,
177
- };
178
- }
179
- // Execute tool with narrowed try-catch for API/network errors only
180
218
  try {
181
219
  let result;
182
220
  switch (name) {
183
221
  case "attrove_query":
184
- result = await (0, index_js_2.executeQueryTool)(client, validatedInput);
222
+ result = await (0, index_js_2.executeQueryTool)(client, validateQueryInput(args));
185
223
  break;
186
224
  case "attrove_search":
187
- result = await (0, index_js_2.executeSearchTool)(client, validatedInput);
225
+ result = await (0, index_js_2.executeSearchTool)(client, validateSearchInput(args));
188
226
  break;
189
227
  case "attrove_integrations":
190
228
  result = await (0, index_js_2.executeIntegrationsTool)(client);
191
229
  break;
230
+ case "attrove_events":
231
+ result = await (0, index_js_2.executeEventsTool)(client, validateEventsInput(args));
232
+ break;
233
+ case "attrove_meetings":
234
+ result = await (0, index_js_2.executeMeetingsTool)(client, validateMeetingsInput(args));
235
+ break;
192
236
  default:
193
- // Already handled above, but TypeScript needs this
194
- throw new Error(`Unexpected tool: ${name}`);
237
+ return {
238
+ content: [
239
+ {
240
+ type: "text",
241
+ text: `Unknown tool: ${name}. Available tools: attrove_query, attrove_search, attrove_integrations, attrove_events, attrove_meetings`,
242
+ },
243
+ ],
244
+ isError: true,
245
+ };
195
246
  }
196
247
  return {
197
- content: [
198
- {
199
- type: "text",
200
- text: result,
201
- },
202
- ],
248
+ content: [{ type: "text", text: result }],
203
249
  };
204
250
  }
205
251
  catch (error) {
206
- // Expected API/network errors - format directly for user
207
- if ((0, sdk_1.isAttroveError)(error) || error instanceof sdk_1.ValidationError) {
252
+ // Validation errors = bad input → propagate as JSON-RPC errors (invalid params)
253
+ if (error instanceof sdk_1.ValidationError) {
254
+ throw error;
255
+ }
256
+ if ((0, sdk_1.isAttroveError)(error)) {
257
+ if (error.status && error.status >= 500) {
258
+ // prettier-ignore
259
+ console.error("[AttroveMCP]", JSON.stringify({ level: "error", msg: "MCP tool received server error from API", errorId: "MCP_TOOL_API_SERVER_ERROR", tool: name, errorCode: error.code, status: error.status }));
260
+ }
261
+ else if (error.status && error.status >= 400) {
262
+ // prettier-ignore
263
+ console.error("[AttroveMCP]", JSON.stringify({ level: "debug", msg: "MCP tool received client error from API", errorId: "MCP_TOOL_API_CLIENT_ERROR", tool: name, errorCode: error.code, status: error.status }));
264
+ }
208
265
  return formatErrorResponse(error);
209
266
  }
210
- // Unexpected errors (programming bugs, system errors) - always log, then format for user.
211
- // We catch all errors to keep the MCP server running, but logging ensures visibility.
267
+ // Unexpected errors log and return to keep the MCP server running
212
268
  // prettier-ignore
213
- console.error('[AttroveMCP] Unexpected error in tool handler:', error);
269
+ console.error("[AttroveMCP]", JSON.stringify({ level: "error", msg: "MCP unexpected error in tool handler", errorId: "MCP_TOOL_UNEXPECTED_ERROR", tool: name, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }));
214
270
  return formatErrorResponse(error);
215
271
  }
216
272
  });
@@ -218,7 +274,7 @@ function createServer(config) {
218
274
  }
219
275
  exports.createServer = createServer;
220
276
  /**
221
- * Start the MCP server with stdio transport.
277
+ * Connect the MCP server to stdin/stdout and begin serving requests.
222
278
  */
223
279
  async function startServer(config) {
224
280
  const server = createServer(config);
@@ -229,7 +285,7 @@ exports.startServer = startServer;
229
285
  /**
230
286
  * Get configuration from environment variables.
231
287
  *
232
- * @throws {Error} If required environment variables are missing
288
+ * @throws {Error} If required environment variables are missing or invalid
233
289
  */
234
290
  function getConfigFromEnv() {
235
291
  const apiKey = process.env.ATTROVE_API_KEY;
@@ -239,12 +295,15 @@ function getConfigFromEnv() {
239
295
  throw new Error("ATTROVE_API_KEY environment variable is required. " +
240
296
  "Set it to your Attrove API key (sk_...).");
241
297
  }
298
+ if (!apiKey.startsWith("sk_")) {
299
+ throw new Error("ATTROVE_API_KEY must start with 'sk_'. Check that you're using a valid API key.");
300
+ }
242
301
  if (!userId) {
243
302
  throw new Error("ATTROVE_USER_ID environment variable is required. " +
244
303
  "Set it to the user ID (UUID) for the user whose context you want to access.");
245
304
  }
246
305
  return {
247
- apiKey,
306
+ apiKey: apiKey,
248
307
  userId,
249
308
  baseUrl,
250
309
  };
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * Events Tool
4
+ *
5
+ * MCP tool for listing calendar events from connected integrations.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.executeEventsTool = exports.eventsToolDefinition = void 0;
9
+ /**
10
+ * MCP schema for discovering and filtering calendar events.
11
+ */
12
+ exports.eventsToolDefinition = {
13
+ name: "attrove_events",
14
+ description: `List calendar events from the user's own connected calendar accounts.
15
+
16
+ This read-only tool returns events from the authenticated user's authorized calendar integrations (e.g., Google Calendar). Only calendars the user has explicitly connected are accessed. Use this when the user asks about:
17
+ - "What's on my calendar today/this week?"
18
+ - "Do I have any meetings tomorrow?"
19
+ - "When is my next meeting with Sarah?"
20
+ - "What's my schedule for Friday?"`,
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: {
24
+ start_date: {
25
+ type: "string",
26
+ description: "Start of date range (ISO 8601). If omitted, the server determines the default.",
27
+ },
28
+ end_date: {
29
+ type: "string",
30
+ description: "End of date range (ISO 8601). If omitted, the server determines the default.",
31
+ },
32
+ limit: {
33
+ type: "number",
34
+ description: "Max events to return (default 25, max 100)",
35
+ },
36
+ },
37
+ required: [],
38
+ },
39
+ annotations: {
40
+ title: "Calendar Events",
41
+ readOnlyHint: true,
42
+ destructiveHint: false,
43
+ idempotentHint: true,
44
+ openWorldHint: true,
45
+ },
46
+ };
47
+ /**
48
+ * Format a calendar event's time range for display (e.g. "Jan 15, 2:00 PM – 3:00 PM").
49
+ * Returns a fallback string when the event contains unparseable dates.
50
+ */
51
+ function formatEventTime(event) {
52
+ if (event.all_day) {
53
+ const start = new Date(event.start_time);
54
+ if (Number.isNaN(start.getTime())) {
55
+ return "All day";
56
+ }
57
+ return `All day (${start.toLocaleDateString()})`;
58
+ }
59
+ const start = new Date(event.start_time);
60
+ const end = new Date(event.end_time);
61
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
62
+ return event.start_time ?? "Unknown time";
63
+ }
64
+ const dateStr = start.toLocaleDateString(undefined, {
65
+ month: "short",
66
+ day: "numeric",
67
+ });
68
+ const startTime = start.toLocaleTimeString(undefined, {
69
+ hour: "numeric",
70
+ minute: "2-digit",
71
+ });
72
+ const endTime = end.toLocaleTimeString(undefined, {
73
+ hour: "numeric",
74
+ minute: "2-digit",
75
+ });
76
+ return `${dateStr}, ${startTime} – ${endTime}`;
77
+ }
78
+ /**
79
+ * Fetch calendar events via the SDK and format them as a human-readable summary.
80
+ */
81
+ async function executeEventsTool(client, input) {
82
+ const options = {
83
+ expand: ["attendees", "location", "description"],
84
+ };
85
+ if (input.start_date) {
86
+ options.startDate = input.start_date;
87
+ }
88
+ if (input.end_date) {
89
+ options.endDate = input.end_date;
90
+ }
91
+ options.limit = Math.max(1, Math.min(input.limit ?? 25, 100));
92
+ const response = await client.events.list(options);
93
+ if (!response.data) {
94
+ // prettier-ignore
95
+ console.error("[AttroveMCP]", JSON.stringify({ level: "warn", msg: "Events API returned success but data was nullish", errorId: "MCP_EVENTS_MALFORMED_RESPONSE" }));
96
+ return "The events API returned an unexpected response format. Please try again.";
97
+ }
98
+ if (response.data.length === 0) {
99
+ return "No calendar events found for the specified date range.";
100
+ }
101
+ const events = response.data;
102
+ let result = `Found ${events.length} calendar event(s):\n`;
103
+ for (const event of events) {
104
+ result += `\n### ${event.title}\n`;
105
+ result += `- **When:** ${formatEventTime(event)}\n`;
106
+ if (event.location) {
107
+ result += `- **Where:** ${event.location}\n`;
108
+ }
109
+ if (event.event_link) {
110
+ result += `- **Meeting link:** ${event.event_link}\n`;
111
+ }
112
+ else if (event.html_link) {
113
+ result += `- **Calendar link:** ${event.html_link}\n`;
114
+ }
115
+ if (event.attendees?.length) {
116
+ const attendeeList = event.attendees
117
+ .map((a) => {
118
+ const name = a.name || a.email;
119
+ const status = a.status ? ` (${a.status})` : "";
120
+ return `${name}${status}`;
121
+ })
122
+ .join(", ");
123
+ result += `- **Attendees:** ${attendeeList}\n`;
124
+ }
125
+ if (event.status && event.status !== "confirmed") {
126
+ result += `- **Status:** ${event.status}\n`;
127
+ }
128
+ }
129
+ if (response.pagination?.has_more) {
130
+ result += `\n_Showing ${events.length} of ${response.pagination.total_count ?? "more"} events. Adjust start_date, end_date, or limit to refine results._\n`;
131
+ }
132
+ return result;
133
+ }
134
+ exports.executeEventsTool = executeEventsTool;
@@ -5,7 +5,7 @@
5
5
  * Tool definitions and executors for the Attrove MCP server.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.allToolDefinitions = exports.executeIntegrationsTool = exports.integrationsToolDefinition = exports.executeSearchTool = exports.searchToolDefinition = exports.executeQueryTool = exports.queryToolDefinition = void 0;
8
+ exports.allToolDefinitions = exports.executeMeetingsTool = exports.meetingsToolDefinition = exports.executeEventsTool = exports.eventsToolDefinition = exports.executeIntegrationsTool = exports.integrationsToolDefinition = exports.executeSearchTool = exports.searchToolDefinition = exports.executeQueryTool = exports.queryToolDefinition = void 0;
9
9
  const query_js_1 = require("./query.js");
10
10
  Object.defineProperty(exports, "queryToolDefinition", { enumerable: true, get: function () { return query_js_1.queryToolDefinition; } });
11
11
  Object.defineProperty(exports, "executeQueryTool", { enumerable: true, get: function () { return query_js_1.executeQueryTool; } });
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "executeSearchTool", { enumerable: true, get: fun
15
15
  const integrations_js_1 = require("./integrations.js");
16
16
  Object.defineProperty(exports, "integrationsToolDefinition", { enumerable: true, get: function () { return integrations_js_1.integrationsToolDefinition; } });
17
17
  Object.defineProperty(exports, "executeIntegrationsTool", { enumerable: true, get: function () { return integrations_js_1.executeIntegrationsTool; } });
18
+ const events_js_1 = require("./events.js");
19
+ Object.defineProperty(exports, "eventsToolDefinition", { enumerable: true, get: function () { return events_js_1.eventsToolDefinition; } });
20
+ Object.defineProperty(exports, "executeEventsTool", { enumerable: true, get: function () { return events_js_1.executeEventsTool; } });
21
+ const meetings_js_1 = require("./meetings.js");
22
+ Object.defineProperty(exports, "meetingsToolDefinition", { enumerable: true, get: function () { return meetings_js_1.meetingsToolDefinition; } });
23
+ Object.defineProperty(exports, "executeMeetingsTool", { enumerable: true, get: function () { return meetings_js_1.executeMeetingsTool; } });
18
24
  /**
19
25
  * All tool definitions for registration with MCP.
20
26
  */
@@ -22,4 +28,6 @@ exports.allToolDefinitions = [
22
28
  query_js_1.queryToolDefinition,
23
29
  search_js_1.searchToolDefinition,
24
30
  integrations_js_1.integrationsToolDefinition,
31
+ events_js_1.eventsToolDefinition,
32
+ meetings_js_1.meetingsToolDefinition,
25
33
  ];
@@ -11,10 +11,10 @@ exports.executeIntegrationsTool = exports.integrationsToolDefinition = void 0;
11
11
  */
12
12
  exports.integrationsToolDefinition = {
13
13
  name: "attrove_integrations",
14
- description: `List the user's connected integrations (Gmail, Slack, etc.).
14
+ description: `List the authenticated user's own connected integrations and their status.
15
15
 
16
- This tool shows which services the user has connected and their current status. Use this to:
17
- - Check what data sources are available for queries
16
+ This read-only tool shows which services the user has explicitly connected to their account and each service's current status. Use this to:
17
+ - Check what data sources the user has authorized
18
18
  - Verify integration status before searching
19
19
  - Help users understand their connected services
20
20
 
@@ -27,9 +27,16 @@ The response includes:
27
27
  properties: {},
28
28
  required: [],
29
29
  },
30
+ annotations: {
31
+ title: "List Integrations",
32
+ readOnlyHint: true,
33
+ destructiveHint: false,
34
+ idempotentHint: true,
35
+ openWorldHint: true,
36
+ },
30
37
  };
31
38
  /**
32
- * Execute the integrations tool.
39
+ * List all connected integrations and format them for MCP display.
33
40
  */
34
41
  async function executeIntegrationsTool(client) {
35
42
  const integrations = await client.integrations.list();