mcp-intervals 1.5.0 → 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 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: "1.0.0",
28
+ version,
25
29
  });
26
30
  registerTools(server, client);
27
31
  registerResources(server, client);
package/dist/tools.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { PDFParse } from "pdf-parse";
3
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
4
8
  export function registerTools(server, client) {
5
9
  // --- get_task ---
6
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).", {
@@ -252,6 +256,17 @@ export function registerTools(server, client) {
252
256
  const docField = docData.document;
253
257
  let document;
254
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
+ }
255
270
  document = docField[0];
256
271
  }
257
272
  else if (docField && typeof docField === "object") {
@@ -262,10 +277,35 @@ export function registerTools(server, client) {
262
277
  }
263
278
  const filename = document.filename || document.title || "unknown";
264
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
+ }
265
294
  const imageMimeType = getMimeTypeFromFilename(filename);
266
295
  if (imageMimeType && isImageMimeType(imageMimeType)) {
267
296
  try {
268
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
+ }
269
309
  const base64 = Buffer.from(buffer).toString("base64");
270
310
  const sizeMB = buffer.byteLength / (1024 * 1024);
271
311
  const content = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-intervals",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "MCP server for Intervals task management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",