mcp-intervals 1.3.2 → 1.4.0
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/client.d.ts +11 -0
- package/dist/client.js +35 -0
- package/dist/tools.js +121 -8
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +19 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class IntervalsClient {
|
|
|
8
8
|
private authHeader;
|
|
9
9
|
constructor(apiToken: string);
|
|
10
10
|
private request;
|
|
11
|
+
private requestBinary;
|
|
11
12
|
getTask(id: number): Promise<Record<string, unknown>>;
|
|
12
13
|
getTaskByLocalId(localId: number): Promise<Record<string, unknown>>;
|
|
13
14
|
resolveTaskId(localId: number): Promise<number>;
|
|
@@ -16,6 +17,16 @@ export declare class IntervalsClient {
|
|
|
16
17
|
addTaskNote(taskId: number, note: string, isPublic?: boolean): Promise<Record<string, unknown>>;
|
|
17
18
|
getProject(id: number): Promise<Record<string, unknown>>;
|
|
18
19
|
getMilestone(id: number): Promise<Record<string, unknown>>;
|
|
20
|
+
getDocuments(params?: {
|
|
21
|
+
taskid?: number;
|
|
22
|
+
projectid?: number;
|
|
23
|
+
personid?: number;
|
|
24
|
+
}): Promise<Record<string, unknown>>;
|
|
25
|
+
getDocument(id: number): Promise<Record<string, unknown>>;
|
|
26
|
+
downloadDocument(id: number): Promise<{
|
|
27
|
+
buffer: ArrayBuffer;
|
|
28
|
+
contentType: string;
|
|
29
|
+
}>;
|
|
19
30
|
getTaskStatuses(): Promise<Record<string, unknown>>;
|
|
20
31
|
getTaskPriorities(): Promise<Record<string, unknown>>;
|
|
21
32
|
getWorkTypes(): Promise<Record<string, unknown>>;
|
package/dist/client.js
CHANGED
|
@@ -31,6 +31,27 @@ export class IntervalsClient {
|
|
|
31
31
|
}
|
|
32
32
|
return response.json();
|
|
33
33
|
}
|
|
34
|
+
async requestBinary(path, params) {
|
|
35
|
+
let url = `${this.baseUrl}${path}`;
|
|
36
|
+
if (params) {
|
|
37
|
+
const searchParams = new URLSearchParams();
|
|
38
|
+
for (const [key, value] of Object.entries(params)) {
|
|
39
|
+
searchParams.set(key, String(value));
|
|
40
|
+
}
|
|
41
|
+
url += `?${searchParams.toString()}`;
|
|
42
|
+
}
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: "GET",
|
|
45
|
+
headers: { Authorization: this.authHeader },
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
throw new Error(`Intervals API error ${response.status}: ${text}`);
|
|
50
|
+
}
|
|
51
|
+
const buffer = await response.arrayBuffer();
|
|
52
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
53
|
+
return { buffer, contentType };
|
|
54
|
+
}
|
|
34
55
|
// --- Task ---
|
|
35
56
|
async getTask(id) {
|
|
36
57
|
const data = await this.request(`/task/${id}/`);
|
|
@@ -73,6 +94,20 @@ export class IntervalsClient {
|
|
|
73
94
|
const data = await this.request(`/milestone/${id}/`);
|
|
74
95
|
return data;
|
|
75
96
|
}
|
|
97
|
+
// --- Documents ---
|
|
98
|
+
async getDocuments(params = {}) {
|
|
99
|
+
const data = await this.request(`/document/`, {
|
|
100
|
+
params: params,
|
|
101
|
+
});
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
async getDocument(id) {
|
|
105
|
+
const data = await this.request(`/document/${id}/`);
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
async downloadDocument(id) {
|
|
109
|
+
return this.requestBinary(`/document/${id}/download/`);
|
|
110
|
+
}
|
|
76
111
|
// --- Resources (statuses & priorities) ---
|
|
77
112
|
async getTaskStatuses() {
|
|
78
113
|
const data = await this.request(`/taskstatus/`);
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { parseTaskIdFromUrl } from "./utils.js";
|
|
2
|
+
import { parseTaskIdFromUrl, getMimeTypeFromFilename, isImageMimeType, } from "./utils.js";
|
|
3
3
|
export function registerTools(server, client) {
|
|
4
4
|
// --- get_task ---
|
|
5
5
|
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).", {
|
|
@@ -9,11 +9,32 @@ export function registerTools(server, client) {
|
|
|
9
9
|
}, async ({ task }) => {
|
|
10
10
|
const localId = parseTaskIdFromUrl(task);
|
|
11
11
|
const data = await client.getTaskByLocalId(localId);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const internalId = Number(data.id);
|
|
13
|
+
const content = [{ type: "text", text: JSON.stringify(data, null, 2) }];
|
|
14
|
+
// Fetch associated documents
|
|
15
|
+
try {
|
|
16
|
+
const docsData = await client.getDocuments({ taskid: internalId });
|
|
17
|
+
const documents = docsData.document;
|
|
18
|
+
if (Array.isArray(documents) && documents.length > 0) {
|
|
19
|
+
const docSummary = documents.map((doc) => ({
|
|
20
|
+
id: doc.id,
|
|
21
|
+
filename: doc.filename,
|
|
22
|
+
title: doc.title,
|
|
23
|
+
dateCreated: doc.datecreated,
|
|
24
|
+
size: doc.filesize,
|
|
25
|
+
}));
|
|
26
|
+
content.push({
|
|
27
|
+
type: "text",
|
|
28
|
+
text: `\n--- Attached Documents (${documents.length}) ---\n` +
|
|
29
|
+
JSON.stringify(docSummary, null, 2) +
|
|
30
|
+
`\n\nUse the download_document tool with the document ID to view image attachments.`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Silently ignore document fetch failures
|
|
36
|
+
}
|
|
37
|
+
return { content };
|
|
17
38
|
});
|
|
18
39
|
// --- update_task ---
|
|
19
40
|
server.tool("update_task", "Update fields on an Intervals task (status, assignee, priority, title, description, due date, owner).", {
|
|
@@ -34,7 +55,7 @@ export function registerTools(server, client) {
|
|
|
34
55
|
summary: z
|
|
35
56
|
.string()
|
|
36
57
|
.optional()
|
|
37
|
-
.describe(
|
|
58
|
+
.describe('New task description/summary (HTML is accepted). IMPORTANT: For checklists, use Intervals-specific HTML — do NOT use emojis (✅, ☐) or markdown checkboxes. Use: <ul class="checklist"><li class="checklist-checked">Done item</li><li>Pending item</li></ul>. Checked items get class="checklist-checked", unchecked items have no class.'),
|
|
38
59
|
datedue: z
|
|
39
60
|
.string()
|
|
40
61
|
.optional()
|
|
@@ -74,7 +95,7 @@ export function registerTools(server, client) {
|
|
|
74
95
|
taskId: z.number().describe("The local task ID (as shown in the Intervals web UI)"),
|
|
75
96
|
note: z
|
|
76
97
|
.string()
|
|
77
|
-
.describe(
|
|
98
|
+
.describe('The note content (HTML is accepted). IMPORTANT: For checklists, use Intervals-specific HTML — do NOT use emojis (✅, ☐) or markdown checkboxes. Use: <ul class="checklist"><li class="checklist-checked">Done item</li><li>Pending item</li></ul>. Checked items get class="checklist-checked", unchecked items have no class.'),
|
|
78
99
|
isPublic: z
|
|
79
100
|
.boolean()
|
|
80
101
|
.default(true)
|
|
@@ -195,4 +216,96 @@ export function registerTools(server, client) {
|
|
|
195
216
|
],
|
|
196
217
|
};
|
|
197
218
|
});
|
|
219
|
+
// --- get_documents ---
|
|
220
|
+
server.tool("get_documents", "List documents/attachments from Intervals. Can filter by task, project, or person. Returns document metadata (filename, title, size, dates) but not file contents. Use download_document to retrieve file contents.", {
|
|
221
|
+
taskId: z
|
|
222
|
+
.number()
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("Filter by local task ID (as shown in the Intervals web UI)"),
|
|
225
|
+
projectId: z.number().optional().describe("Filter by project ID"),
|
|
226
|
+
personId: z.number().optional().describe("Filter by person ID"),
|
|
227
|
+
}, async ({ taskId, projectId, personId }) => {
|
|
228
|
+
let internalTaskId;
|
|
229
|
+
if (taskId) {
|
|
230
|
+
internalTaskId = await client.resolveTaskId(taskId);
|
|
231
|
+
}
|
|
232
|
+
const data = await client.getDocuments({
|
|
233
|
+
taskid: internalTaskId,
|
|
234
|
+
projectid: projectId,
|
|
235
|
+
personid: personId,
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
content: [
|
|
239
|
+
{ type: "text", text: JSON.stringify(data, null, 2) },
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
// --- 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.", {
|
|
245
|
+
documentId: z
|
|
246
|
+
.number()
|
|
247
|
+
.describe("The numeric document ID from Intervals"),
|
|
248
|
+
}, async ({ documentId }) => {
|
|
249
|
+
// Get document metadata to know the filename
|
|
250
|
+
const docData = await client.getDocument(documentId);
|
|
251
|
+
const document = docData.document?.[0] ??
|
|
252
|
+
docData;
|
|
253
|
+
const filename = document.filename || "unknown";
|
|
254
|
+
const title = document.title || filename;
|
|
255
|
+
const imageMimeType = getMimeTypeFromFilename(filename);
|
|
256
|
+
if (imageMimeType && isImageMimeType(imageMimeType)) {
|
|
257
|
+
try {
|
|
258
|
+
const { buffer } = await client.downloadDocument(documentId);
|
|
259
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
260
|
+
const sizeMB = buffer.byteLength / (1024 * 1024);
|
|
261
|
+
const content = [
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: `Document: ${title} (${filename}, ${sizeMB.toFixed(2)} MB)`,
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
if (imageMimeType === "image/svg+xml") {
|
|
268
|
+
const svgText = new TextDecoder().decode(buffer);
|
|
269
|
+
content.push({
|
|
270
|
+
type: "text",
|
|
271
|
+
text: `SVG content:\n${svgText}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
content.push({
|
|
276
|
+
type: "image",
|
|
277
|
+
data: base64,
|
|
278
|
+
mimeType: imageMimeType,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return { content };
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `Document "${title}" (${filename}) is an image but could not be downloaded: ${err instanceof Error ? err.message : String(err)}`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
isError: true,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Non-image file: return metadata only
|
|
296
|
+
return {
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: JSON.stringify({
|
|
301
|
+
documentId,
|
|
302
|
+
filename,
|
|
303
|
+
title,
|
|
304
|
+
message: "This file type cannot be rendered inline.",
|
|
305
|
+
...document,
|
|
306
|
+
}, null, 2),
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
});
|
|
198
311
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -4,3 +4,5 @@
|
|
|
4
4
|
* Also accepts a raw numeric string/number as fallback.
|
|
5
5
|
*/
|
|
6
6
|
export declare function parseTaskIdFromUrl(input: string): number;
|
|
7
|
+
export declare function getMimeTypeFromFilename(filename: string): string | null;
|
|
8
|
+
export declare function isImageMimeType(mimeType: string): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -14,3 +14,22 @@ export function parseTaskIdFromUrl(input) {
|
|
|
14
14
|
}
|
|
15
15
|
throw new Error(`Cannot parse task ID from: "${input}". Expected a URL like https://<subdomain>.intervalsonline.com/tasks/view/<id> or a numeric ID.`);
|
|
16
16
|
}
|
|
17
|
+
// --- Image MIME type helpers ---
|
|
18
|
+
const IMAGE_EXTENSIONS = {
|
|
19
|
+
".png": "image/png",
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".jpeg": "image/jpeg",
|
|
22
|
+
".gif": "image/gif",
|
|
23
|
+
".webp": "image/webp",
|
|
24
|
+
".svg": "image/svg+xml",
|
|
25
|
+
".bmp": "image/bmp",
|
|
26
|
+
".ico": "image/x-icon",
|
|
27
|
+
};
|
|
28
|
+
const IMAGE_MIME_TYPES = new Set(Object.values(IMAGE_EXTENSIONS));
|
|
29
|
+
export function getMimeTypeFromFilename(filename) {
|
|
30
|
+
const ext = filename.toLowerCase().match(/\.[^.]+$/)?.[0];
|
|
31
|
+
return ext ? IMAGE_EXTENSIONS[ext] ?? null : null;
|
|
32
|
+
}
|
|
33
|
+
export function isImageMimeType(mimeType) {
|
|
34
|
+
return IMAGE_MIME_TYPES.has(mimeType);
|
|
35
|
+
}
|