mcp-intervals 1.4.1 → 1.5.1
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/dist/cli/api.js +1 -1
- package/dist/cli/init.js +0 -0
- package/dist/client.js +5 -2
- package/dist/index.js +5 -1
- package/dist/tools.js +71 -2
- package/package.json +2 -1
package/dist/cli/api.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function validateToken(token) {
|
|
|
11
11
|
const data = (await response.json());
|
|
12
12
|
return {
|
|
13
13
|
valid: true,
|
|
14
|
-
workspace: data.me?.company || "Unknown workspace",
|
|
14
|
+
workspace: data.me?.[0]?.company || "Unknown workspace",
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
if (response.status === 401) {
|
package/dist/cli/init.js
CHANGED
|
File without changes
|
package/dist/client.js
CHANGED
|
@@ -27,7 +27,7 @@ export class IntervalsClient {
|
|
|
27
27
|
const response = await fetch(url, fetchOptions);
|
|
28
28
|
if (!response.ok) {
|
|
29
29
|
const text = await response.text();
|
|
30
|
-
throw new Error(`Intervals API error ${response.status}: ${text}`);
|
|
30
|
+
throw new Error(`Intervals API error ${response.status}: ${text.slice(0, 500)}`);
|
|
31
31
|
}
|
|
32
32
|
return response.json();
|
|
33
33
|
}
|
|
@@ -127,6 +127,9 @@ export class IntervalsClient {
|
|
|
127
127
|
// First get the current user's person ID
|
|
128
128
|
const me = await this.getMe();
|
|
129
129
|
const personid = me.personid;
|
|
130
|
+
if (typeof personid !== "number") {
|
|
131
|
+
throw new Error("Could not determine current user's person ID from Intervals /me/ response.");
|
|
132
|
+
}
|
|
130
133
|
const data = await this.request(`/time/`, {
|
|
131
134
|
method: "POST",
|
|
132
135
|
body: {
|
|
@@ -149,6 +152,6 @@ export class IntervalsClient {
|
|
|
149
152
|
async getMe() {
|
|
150
153
|
const data = await this.request(`/me/`);
|
|
151
154
|
// Return the first user object with personid from the top-level response
|
|
152
|
-
return { ...data.me[0], personid: data.personid };
|
|
155
|
+
return { ...(data.me?.[0] ?? {}), personid: data.personid };
|
|
153
156
|
}
|
|
154
157
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,10 @@ async function startServer() {
|
|
|
13
13
|
const { IntervalsClient } = await import("./client.js");
|
|
14
14
|
const { registerTools } = await import("./tools.js");
|
|
15
15
|
const { registerResources } = await import("./resources.js");
|
|
16
|
+
const { createRequire } = await import("node:module");
|
|
17
|
+
// Read version from package.json so it never drifts from the published one.
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { version } = require("../package.json");
|
|
16
20
|
const API_TOKEN = process.env.INTERVALS_API_TOKEN;
|
|
17
21
|
if (!API_TOKEN) {
|
|
18
22
|
console.error("Error: INTERVALS_API_TOKEN environment variable is required.");
|
|
@@ -21,7 +25,7 @@ async function startServer() {
|
|
|
21
25
|
const client = new IntervalsClient(API_TOKEN);
|
|
22
26
|
const server = new McpServer({
|
|
23
27
|
name: "mcp-intervals",
|
|
24
|
-
version
|
|
28
|
+
version,
|
|
25
29
|
});
|
|
26
30
|
registerTools(server, client);
|
|
27
31
|
registerResources(server, client);
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { PDFParse } from "pdf-parse";
|
|
2
3
|
import { parseTaskIdFromUrl, getMimeTypeFromFilename, isImageMimeType, } from "./utils.js";
|
|
4
|
+
// Cap on inline-rendered downloads (images/PDFs). Base64 inflates payload
|
|
5
|
+
// ~33% and the result is fed into the model context, so guard against
|
|
6
|
+
// flooding memory/context with oversized attachments.
|
|
7
|
+
const MAX_INLINE_DOWNLOAD_BYTES = 25 * 1024 * 1024; // 25 MB
|
|
3
8
|
export function registerTools(server, client) {
|
|
4
9
|
// --- get_task ---
|
|
5
10
|
server.tool("get_task", "Retrieve full details of an Intervals task. Accepts a task URL (e.g. https://<subdomain>.intervalsonline.com/tasks/view/12345) or a numeric local task ID (the ID shown in the Intervals web UI).", {
|
|
@@ -241,7 +246,7 @@ export function registerTools(server, client) {
|
|
|
241
246
|
};
|
|
242
247
|
});
|
|
243
248
|
// --- download_document ---
|
|
244
|
-
server.tool("download_document", "Download a document from Intervals. For images (png, jpg, gif, webp, bmp), returns the image inline so it can be viewed directly. For other file types, returns the document metadata.", {
|
|
249
|
+
server.tool("download_document", "Download a document from Intervals. For images (png, jpg, gif, webp, bmp), returns the image inline so it can be viewed directly. For PDFs, extracts and returns the text content. For other file types, returns the document metadata.", {
|
|
245
250
|
documentId: z
|
|
246
251
|
.number()
|
|
247
252
|
.describe("The numeric document ID from Intervals"),
|
|
@@ -251,6 +256,17 @@ export function registerTools(server, client) {
|
|
|
251
256
|
const docField = docData.document;
|
|
252
257
|
let document;
|
|
253
258
|
if (Array.isArray(docField)) {
|
|
259
|
+
if (docField.length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: `No document found with ID ${documentId}.`,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
isError: true,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
254
270
|
document = docField[0];
|
|
255
271
|
}
|
|
256
272
|
else if (docField && typeof docField === "object") {
|
|
@@ -261,10 +277,35 @@ export function registerTools(server, client) {
|
|
|
261
277
|
}
|
|
262
278
|
const filename = document.filename || document.title || "unknown";
|
|
263
279
|
const title = document.title || filename;
|
|
280
|
+
// Reject oversized files before downloading them into memory.
|
|
281
|
+
const reportedSize = Number(document.filesize);
|
|
282
|
+
if (Number.isFinite(reportedSize) &&
|
|
283
|
+
reportedSize > MAX_INLINE_DOWNLOAD_BYTES) {
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `Document "${title}" (${filename}) is ${(reportedSize / (1024 * 1024)).toFixed(2)} MB, exceeding the ${MAX_INLINE_DOWNLOAD_BYTES / (1024 * 1024)} MB inline limit. Download it directly from Intervals instead.`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
isError: true,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
264
294
|
const imageMimeType = getMimeTypeFromFilename(filename);
|
|
265
295
|
if (imageMimeType && isImageMimeType(imageMimeType)) {
|
|
266
296
|
try {
|
|
267
297
|
const { buffer } = await client.downloadDocument(documentId);
|
|
298
|
+
if (buffer.byteLength > MAX_INLINE_DOWNLOAD_BYTES) {
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: `Document "${title}" (${filename}) is ${(buffer.byteLength / (1024 * 1024)).toFixed(2)} MB, exceeding the ${MAX_INLINE_DOWNLOAD_BYTES / (1024 * 1024)} MB inline limit. Download it directly from Intervals instead.`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
268
309
|
const base64 = Buffer.from(buffer).toString("base64");
|
|
269
310
|
const sizeMB = buffer.byteLength / (1024 * 1024);
|
|
270
311
|
const content = [
|
|
@@ -301,7 +342,35 @@ export function registerTools(server, client) {
|
|
|
301
342
|
};
|
|
302
343
|
}
|
|
303
344
|
}
|
|
304
|
-
//
|
|
345
|
+
// PDF: extract text content
|
|
346
|
+
if (filename.toLowerCase().endsWith(".pdf")) {
|
|
347
|
+
try {
|
|
348
|
+
const { buffer } = await client.downloadDocument(documentId);
|
|
349
|
+
const sizeMB = buffer.byteLength / (1024 * 1024);
|
|
350
|
+
const pdf = new PDFParse({ data: new Uint8Array(buffer) });
|
|
351
|
+
const textResult = await pdf.getText();
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: "text",
|
|
356
|
+
text: `Document: ${title} (${filename}, ${sizeMB.toFixed(2)} MB, ${textResult.total} pages)\n\n${textResult.text}`,
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: `Document "${title}" (${filename}) is a PDF but text extraction failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
isError: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Other file types: return metadata only
|
|
305
374
|
return {
|
|
306
375
|
content: [
|
|
307
376
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-intervals",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "MCP server for Intervals task management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.0.0",
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
34
|
+
"pdf-parse": "^2.4.5",
|
|
34
35
|
"picocolors": "^1.1.1",
|
|
35
36
|
"zod": "^3.24.2"
|
|
36
37
|
},
|