@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.
- package/README.md +101 -3
- package/cjs/index.js +6 -1
- package/cjs/server.js +115 -56
- package/cjs/tools/events.js +134 -0
- package/cjs/tools/index.js +9 -1
- package/cjs/tools/integrations.js +11 -4
- 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 +116 -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 +3 -10
- package/esm/tools/integrations.d.ts.map +1 -1
- package/esm/tools/integrations.js +11 -4
- 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 +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 (
|
|
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:
|
|
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:
|
|
71
|
-
before_date:
|
|
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:
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
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,
|
|
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,
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
207
|
-
if (
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
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;
|
package/cjs/tools/index.js
CHANGED
|
@@ -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
|
|
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
|
|
17
|
-
- Check what data sources
|
|
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
|
-
*
|
|
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();
|