@attrove/mcp 0.1.7 → 0.1.9

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 +153 -9
  2. package/cjs/index.js +6 -1
  3. package/cjs/server.js +118 -56
  4. package/cjs/tools/events.js +134 -0
  5. package/cjs/tools/index.js +9 -1
  6. package/cjs/tools/integrations.js +12 -5
  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 +119 -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 +4 -11
  28. package/esm/tools/integrations.d.ts.map +1 -1
  29. package/esm/tools/integrations.js +12 -5
  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 +8 -5
package/README.md CHANGED
@@ -16,14 +16,29 @@ pnpm add @attrove/mcp
16
16
 
17
17
  ### Claude Desktop
18
18
 
19
- Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
19
+ Claude Desktop supports two transport options:
20
+
21
+ **HTTP transport (recommended)** — uses OAuth 2.1, no API key needed:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "attrove": {
27
+ "type": "streamable-http",
28
+ "url": "https://api.attrove.com/mcp"
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ **Stdio transport** — add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
20
35
 
21
36
  ```json
22
37
  {
23
38
  "mcpServers": {
24
39
  "attrove": {
25
40
  "command": "npx",
26
- "args": ["@attrove/mcp"],
41
+ "args": ["-y", "@attrove/mcp@latest"],
27
42
  "env": {
28
43
  "ATTROVE_API_KEY": "sk_...",
29
44
  "ATTROVE_USER_ID": "user-uuid"
@@ -44,7 +59,7 @@ Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/
44
59
  "mcpServers": {
45
60
  "attrove": {
46
61
  "command": "npx",
47
- "args": ["@attrove/mcp"],
62
+ "args": ["-y", "@attrove/mcp@latest"],
48
63
  "env": {
49
64
  "ATTROVE_API_KEY": "sk_...",
50
65
  "ATTROVE_USER_ID": "user-uuid"
@@ -66,6 +81,33 @@ export ATTROVE_API_KEY="sk_..."
66
81
  export ATTROVE_USER_ID="user-uuid"
67
82
  ```
68
83
 
84
+ ### ChatGPT (via HTTP endpoint)
85
+
86
+ ChatGPT and other AI assistants that support MCP connectors can connect via the hosted HTTP endpoint.
87
+
88
+ > **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.
89
+
90
+ **Basic requirements:**
91
+ - HTTP endpoint URL: `https://api.attrove.com/mcp`
92
+ - Authentication: Bearer token (`sk_...`)
93
+ - Custom header: `X-Attrove-User-Id` with your user UUID
94
+
95
+ **Example setup steps (may vary):**
96
+ 1. Open ChatGPT Settings → **Connectors** → Enable **Developer Mode**
97
+ 2. Click **Create Connector** and configure:
98
+ - **Name:** `Attrove`
99
+ - **URL:** `https://api.attrove.com/mcp`
100
+ - **Authentication:** Bearer token
101
+ - **Token:** Your API key (`sk_...`)
102
+ 3. Add custom header:
103
+ - **Header:** `X-Attrove-User-Id`
104
+ - **Value:** Your user ID (UUID)
105
+
106
+ Once connected, you can ask ChatGPT questions like:
107
+ - "What emails need my attention this week?"
108
+ - "Summarize my meeting with the marketing team"
109
+ - "What has John been asking about lately?"
110
+
69
111
  ### Direct CLI Usage
70
112
 
71
113
  ```bash
@@ -99,8 +141,8 @@ Ask questions about the user's communications and get AI-generated answers.
99
141
 
100
142
  **Parameters:**
101
143
  - `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
144
+ - `integration_ids` (optional): Filter to specific integration IDs (array of UUID strings)
145
+ - `include_sources` (optional): Include source snippets in the response
104
146
 
105
147
  **Example prompts:**
106
148
  - "What did Sarah say about the Q4 budget?"
@@ -118,7 +160,7 @@ Search for specific messages or conversations.
118
160
  - `after_date` (optional): Only messages after this date (ISO 8601)
119
161
  - `before_date` (optional): Only messages before this date
120
162
  - `sender_domains` (optional): Filter by sender domains
121
- - `include_body_text` (optional): Include message content (default: true)
163
+ - `include_body_text` (optional): Include message content in results (default: true, bodies truncated to 200 characters)
122
164
 
123
165
  **Example prompts:**
124
166
  - "Find all emails about the product launch"
@@ -136,6 +178,37 @@ List the user's connected integrations.
136
178
  - "What services are connected?"
137
179
  - "Show me my integrations"
138
180
 
181
+ ### `attrove_events`
182
+
183
+ List calendar events from the user's connected calendar accounts.
184
+
185
+ **Parameters:**
186
+ - `start_date` (optional): Start of date range (ISO 8601)
187
+ - `end_date` (optional): End of date range (ISO 8601)
188
+ - `limit` (optional): Max events to return (default 25, max 100)
189
+
190
+ **Example prompts:**
191
+ - "What's on my calendar today?"
192
+ - "Do I have any meetings tomorrow?"
193
+ - "When is my next meeting with Sarah?"
194
+ - "What's my schedule for Friday?"
195
+
196
+ ### `attrove_meetings`
197
+
198
+ List meetings with AI-generated summaries and action items.
199
+
200
+ **Parameters:**
201
+ - `start_date` (optional): Start of date range (ISO 8601)
202
+ - `end_date` (optional): End of date range (ISO 8601)
203
+ - `provider` (optional): Filter by meeting provider (`google_meet`, `zoom`, `teams`)
204
+ - `limit` (optional): Max meetings to return (default 10, max 50)
205
+
206
+ **Example prompts:**
207
+ - "What happened in my last meeting?"
208
+ - "Summarize yesterday's standup"
209
+ - "What are the action items from the product review?"
210
+ - "Show me my recent meetings"
211
+
139
212
  ## Environment Variables
140
213
 
141
214
  | Variable | Required | Description |
@@ -165,6 +238,77 @@ await startServer({
165
238
  });
166
239
  ```
167
240
 
241
+ ### HTTP Endpoint (Hosted)
242
+
243
+ For AI assistants that connect via HTTP (like ChatGPT), use the hosted endpoint:
244
+
245
+ ```bash
246
+ # Test the endpoint
247
+ curl -X POST https://api.attrove.com/mcp \
248
+ -H "Authorization: Bearer sk_..." \
249
+ -H "X-Attrove-User-Id: user-uuid" \
250
+ -H "Content-Type: application/json" \
251
+ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
252
+ ```
253
+
254
+ Or integrate in your own server using the HTTP handler:
255
+
256
+ ```typescript
257
+ import { createHttpHandler } from '@attrove/mcp';
258
+
259
+ const handler = createHttpHandler(
260
+ {
261
+ apiKey: 'sk_...',
262
+ userId: 'user-uuid',
263
+ baseUrl: 'https://api.attrove.com', // optional: custom API endpoint
264
+ },
265
+ {
266
+ enableJsonResponse: true, // optional: use JSON instead of SSE (default: true)
267
+ timeoutMs: 30000, // optional: request timeout in ms (default: 30000)
268
+ }
269
+ );
270
+
271
+ // With Fastify (recommended)
272
+ fastify.post('/mcp', async (request, reply) => {
273
+ const result = await handler.handleRequest(request.raw, reply.raw, request.body);
274
+
275
+ if (!result.handled) {
276
+ // Handle timeout with 504, other errors with 500
277
+ const statusCode = result.isTimeout ? 504 : 500;
278
+ const userMessage = result.isTimeout
279
+ ? 'Request timed out. Try a simpler query or reduce the scope.'
280
+ : 'An unexpected error occurred. Please try again.';
281
+
282
+ // Only send response if headers haven't been sent (e.g., during streaming)
283
+ if (!reply.raw.headersSent) {
284
+ reply.code(statusCode).send({
285
+ success: false,
286
+ error: { code: result.isTimeout ? 'REQUEST_TIMEOUT' : 'INTERNAL_ERROR', message: userMessage }
287
+ });
288
+ } else if (!reply.raw.writableEnded) {
289
+ reply.raw.end(); // Ensure stream is closed
290
+ }
291
+ return;
292
+ }
293
+
294
+ // Optional: monitor cleanup failures for resource leak detection
295
+ if (result.cleanupFailed) {
296
+ console.warn('MCP cleanup failed - potential resource leak');
297
+ }
298
+ });
299
+
300
+ // With raw Node.js HTTP server
301
+ import { createServer } from 'node:http';
302
+ const server = createServer(async (req, res) => {
303
+ // Note: You'll need to parse the body yourself for raw HTTP
304
+ const result = await handler.handleRequest(req, res);
305
+ if (!result.handled) {
306
+ res.writeHead(500, { 'Content-Type': 'application/json' });
307
+ res.end(JSON.stringify({ error: result.error }));
308
+ }
309
+ });
310
+ ```
311
+
168
312
  ## Getting API Credentials
169
313
 
170
314
  1. Sign up at [attrove.com](https://attrove.com)
@@ -209,7 +353,7 @@ Set `ATTROVE_DEBUG=true` to enable verbose error logging with stack traces:
209
353
  "mcpServers": {
210
354
  "attrove": {
211
355
  "command": "npx",
212
- "args": ["@attrove/mcp"],
356
+ "args": ["-y", "@attrove/mcp@latest"],
213
357
  "env": {
214
358
  "ATTROVE_API_KEY": "sk_...",
215
359
  "ATTROVE_USER_ID": "user-uuid",
@@ -233,14 +377,14 @@ The Attrove API has rate limits. If you're making many requests, you may need to
233
377
  For AI assistants and code generation tools, Attrove provides machine-readable documentation:
234
378
 
235
379
  - **llms.txt**: `https://attrove.com/llms.txt` - Condensed API reference for LLMs
236
- - **Quickstart**: `https://github.com/attrove/quickstart` - Example code with CLAUDE.md context
380
+ - **Examples**: `https://github.com/attrove/examples` - Example code with CLAUDE.md context
237
381
 
238
382
  ## Links
239
383
 
240
384
  - [Documentation](https://docs.attrove.com)
241
385
  - [API Reference](https://docs.attrove.com/api)
242
386
  - [TypeScript SDK](https://www.npmjs.com/package/@attrove/sdk)
243
- - [Quickstart Examples](https://github.com/attrove/quickstart)
387
+ - [Examples](https://github.com/attrove/examples)
244
388
 
245
389
  ## License
246
390
 
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,14 @@ 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 &&
48
+ typeof input.include_sources !== "boolean") {
49
+ 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 });
50
+ }
47
51
  return {
48
52
  query: input.query,
49
53
  integration_ids: validateStringArray(input.integration_ids, "integration_ids"),
50
- include_sources: typeof input.include_sources === "boolean"
51
- ? input.include_sources
52
- : undefined,
54
+ include_sources: input.include_sources,
53
55
  };
54
56
  }
55
57
  /**
@@ -65,14 +67,81 @@ function validateSearchInput(args) {
65
67
  if (typeof input.query !== "string" || input.query.trim() === "") {
66
68
  throw new sdk_1.ValidationError('Missing required parameter "query". Please provide a search query.', sdk_1.ErrorCodes.VALIDATION_REQUIRED_FIELD, { field: "query" });
67
69
  }
70
+ if (input.after_date !== undefined && typeof input.after_date !== "string") {
71
+ 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 });
72
+ }
73
+ if (input.before_date !== undefined &&
74
+ typeof input.before_date !== "string") {
75
+ 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 });
76
+ }
77
+ if (input.include_body_text !== undefined &&
78
+ typeof input.include_body_text !== "boolean") {
79
+ 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 });
80
+ }
68
81
  return {
69
82
  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,
83
+ after_date: input.after_date,
84
+ before_date: input.before_date,
72
85
  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,
86
+ include_body_text: input.include_body_text,
87
+ };
88
+ }
89
+ /**
90
+ * Validate input for the events tool.
91
+ * All parameters are optional, so validation is light type checking.
92
+ */
93
+ function validateEventsInput(args) {
94
+ if (args !== undefined && args !== null && typeof args !== "object") {
95
+ throw new sdk_1.ValidationError("Invalid arguments. Expected an object or empty.", sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { expected: "object", received: typeof args });
96
+ }
97
+ const input = (args ?? {});
98
+ if (input.start_date !== undefined && typeof input.start_date !== "string") {
99
+ 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 });
100
+ }
101
+ if (input.end_date !== undefined && typeof input.end_date !== "string") {
102
+ 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 });
103
+ }
104
+ if (input.limit !== undefined && typeof input.limit !== "number") {
105
+ 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 });
106
+ }
107
+ return {
108
+ start_date: input.start_date,
109
+ end_date: input.end_date,
110
+ limit: input.limit,
111
+ };
112
+ }
113
+ /**
114
+ * Validate input for the meetings tool.
115
+ * All parameters are optional, so validation is light type checking.
116
+ */
117
+ function validateMeetingsInput(args) {
118
+ if (args !== undefined && args !== null && typeof args !== "object") {
119
+ throw new sdk_1.ValidationError("Invalid arguments. Expected an object or empty.", sdk_1.ErrorCodes.VALIDATION_INVALID_FORMAT, { expected: "object", received: typeof args });
120
+ }
121
+ const input = (args ?? {});
122
+ const validProviders = ["google_meet", "zoom", "teams"];
123
+ let provider;
124
+ if (input.provider !== undefined && input.provider !== null) {
125
+ if (typeof input.provider !== "string" ||
126
+ !validProviders.includes(input.provider)) {
127
+ 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 });
128
+ }
129
+ provider = input.provider;
130
+ }
131
+ if (input.start_date !== undefined && typeof input.start_date !== "string") {
132
+ 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 });
133
+ }
134
+ if (input.end_date !== undefined && typeof input.end_date !== "string") {
135
+ 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 });
136
+ }
137
+ if (input.limit !== undefined && typeof input.limit !== "number") {
138
+ 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 });
139
+ }
140
+ return {
141
+ start_date: input.start_date,
142
+ end_date: input.end_date,
143
+ provider,
144
+ limit: input.limit,
76
145
  };
77
146
  }
78
147
  /**
@@ -121,7 +190,7 @@ function formatErrorResponse(error) {
121
190
  };
122
191
  }
123
192
  /**
124
- * Create and configure the Attrove MCP server.
193
+ * Wire up tool definitions and dispatch logic for the Attrove MCP server.
125
194
  */
126
195
  function createServer(config) {
127
196
  const server = new index_js_1.Server({
@@ -132,85 +201,75 @@ function createServer(config) {
132
201
  tools: {},
133
202
  },
134
203
  });
135
- // Create Attrove client
136
- // Type assertion is safe because startServer validates the apiKey prefix
137
204
  const client = new sdk_1.Attrove({
138
205
  apiKey: config.apiKey,
139
206
  userId: config.userId,
140
207
  baseUrl: config.baseUrl,
141
208
  });
142
- // Register tool list handler
143
209
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
144
210
  return {
145
211
  tools: index_js_2.allToolDefinitions.map((tool) => ({
146
212
  name: tool.name,
147
213
  description: tool.description,
148
214
  inputSchema: tool.inputSchema,
215
+ annotations: tool.annotations,
149
216
  })),
150
217
  };
151
218
  });
152
- // Register tool call handler
153
219
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
154
220
  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
221
  try {
181
222
  let result;
182
223
  switch (name) {
183
224
  case "attrove_query":
184
- result = await (0, index_js_2.executeQueryTool)(client, validatedInput);
225
+ result = await (0, index_js_2.executeQueryTool)(client, validateQueryInput(args));
185
226
  break;
186
227
  case "attrove_search":
187
- result = await (0, index_js_2.executeSearchTool)(client, validatedInput);
228
+ result = await (0, index_js_2.executeSearchTool)(client, validateSearchInput(args));
188
229
  break;
189
230
  case "attrove_integrations":
190
231
  result = await (0, index_js_2.executeIntegrationsTool)(client);
191
232
  break;
233
+ case "attrove_events":
234
+ result = await (0, index_js_2.executeEventsTool)(client, validateEventsInput(args));
235
+ break;
236
+ case "attrove_meetings":
237
+ result = await (0, index_js_2.executeMeetingsTool)(client, validateMeetingsInput(args));
238
+ break;
192
239
  default:
193
- // Already handled above, but TypeScript needs this
194
- throw new Error(`Unexpected tool: ${name}`);
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: `Unknown tool: ${name}. Available tools: attrove_query, attrove_search, attrove_integrations, attrove_events, attrove_meetings`,
245
+ },
246
+ ],
247
+ isError: true,
248
+ };
195
249
  }
196
250
  return {
197
- content: [
198
- {
199
- type: "text",
200
- text: result,
201
- },
202
- ],
251
+ content: [{ type: "text", text: result }],
203
252
  };
204
253
  }
205
254
  catch (error) {
206
- // Expected API/network errors - format directly for user
207
- if ((0, sdk_1.isAttroveError)(error) || error instanceof sdk_1.ValidationError) {
255
+ // Validation errors = bad input → propagate as JSON-RPC errors (invalid params)
256
+ if (error instanceof sdk_1.ValidationError) {
257
+ throw error;
258
+ }
259
+ if ((0, sdk_1.isAttroveError)(error)) {
260
+ if (error.status && error.status >= 500) {
261
+ // prettier-ignore
262
+ 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 }));
263
+ }
264
+ else if (error.status && error.status >= 400) {
265
+ // prettier-ignore
266
+ 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 }));
267
+ }
208
268
  return formatErrorResponse(error);
209
269
  }
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.
270
+ // Unexpected errors log and return to keep the MCP server running
212
271
  // prettier-ignore
213
- console.error('[AttroveMCP] Unexpected error in tool handler:', error);
272
+ 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
273
  return formatErrorResponse(error);
215
274
  }
216
275
  });
@@ -218,7 +277,7 @@ function createServer(config) {
218
277
  }
219
278
  exports.createServer = createServer;
220
279
  /**
221
- * Start the MCP server with stdio transport.
280
+ * Connect the MCP server to stdin/stdout and begin serving requests.
222
281
  */
223
282
  async function startServer(config) {
224
283
  const server = createServer(config);
@@ -229,7 +288,7 @@ exports.startServer = startServer;
229
288
  /**
230
289
  * Get configuration from environment variables.
231
290
  *
232
- * @throws {Error} If required environment variables are missing
291
+ * @throws {Error} If required environment variables are missing or invalid
233
292
  */
234
293
  function getConfigFromEnv() {
235
294
  const apiKey = process.env.ATTROVE_API_KEY;
@@ -239,12 +298,15 @@ function getConfigFromEnv() {
239
298
  throw new Error("ATTROVE_API_KEY environment variable is required. " +
240
299
  "Set it to your Attrove API key (sk_...).");
241
300
  }
301
+ if (!apiKey.startsWith("sk_")) {
302
+ throw new Error("ATTROVE_API_KEY must start with 'sk_'. Check that you're using a valid API key.");
303
+ }
242
304
  if (!userId) {
243
305
  throw new Error("ATTROVE_USER_ID environment variable is required. " +
244
306
  "Set it to the user ID (UUID) for the user whose context you want to access.");
245
307
  }
246
308
  return {
247
- apiKey,
309
+ apiKey: apiKey,
248
310
  userId,
249
311
  baseUrl,
250
312
  };
@@ -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;