@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.
- package/README.md +153 -9
- package/cjs/index.js +6 -1
- package/cjs/server.js +118 -56
- package/cjs/tools/events.js +134 -0
- package/cjs/tools/index.js +9 -1
- package/cjs/tools/integrations.js +12 -5
- package/cjs/tools/meetings.js +149 -0
- package/cjs/tools/query.js +12 -5
- package/cjs/tools/search.js +21 -7
- package/cjs/transport/http.js +192 -0
- package/esm/index.d.ts +3 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -1
- package/esm/index.js.map +1 -1
- package/esm/server.d.ts +7 -5
- package/esm/server.d.ts.map +1 -1
- package/esm/server.js +119 -57
- package/esm/server.js.map +1 -1
- package/esm/tools/events.d.ts +24 -0
- package/esm/tools/events.d.ts.map +1 -0
- package/esm/tools/events.js +131 -0
- package/esm/tools/events.js.map +1 -0
- package/esm/tools/index.d.ts +6 -69
- package/esm/tools/index.d.ts.map +1 -1
- package/esm/tools/index.js +5 -2
- package/esm/tools/index.js.map +1 -1
- package/esm/tools/integrations.d.ts +4 -11
- package/esm/tools/integrations.d.ts.map +1 -1
- package/esm/tools/integrations.js +12 -5
- package/esm/tools/integrations.js.map +1 -1
- package/esm/tools/meetings.d.ts +26 -0
- package/esm/tools/meetings.d.ts.map +1 -0
- package/esm/tools/meetings.js +146 -0
- package/esm/tools/meetings.js.map +1 -0
- package/esm/tools/query.d.ts +3 -27
- package/esm/tools/query.d.ts.map +1 -1
- package/esm/tools/query.js +12 -5
- package/esm/tools/query.js.map +1 -1
- package/esm/tools/search.d.ts +3 -35
- package/esm/tools/search.d.ts.map +1 -1
- package/esm/tools/search.js +21 -7
- package/esm/tools/search.js.map +1 -1
- package/esm/transport/http.d.ts +63 -0
- package/esm/transport/http.d.ts.map +1 -0
- package/esm/transport/http.js +189 -0
- package/esm/transport/http.js.map +1 -0
- 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
|
-
|
|
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 (
|
|
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
|
-
- **
|
|
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
|
-
- [
|
|
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:
|
|
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:
|
|
71
|
-
before_date:
|
|
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:
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
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,
|
|
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,
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
207
|
-
if (
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
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;
|