mcp-intervals 1.0.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.
@@ -0,0 +1,21 @@
1
+ export interface IntervalsRequestOptions {
2
+ method?: "GET" | "POST" | "PUT" | "DELETE";
3
+ body?: Record<string, unknown>;
4
+ params?: Record<string, string | number | boolean>;
5
+ }
6
+ export declare class IntervalsClient {
7
+ private baseUrl;
8
+ private authHeader;
9
+ constructor(apiToken: string);
10
+ private request;
11
+ getTask(id: number): Promise<Record<string, unknown>>;
12
+ getTaskByLocalId(localId: number): Promise<Record<string, unknown>>;
13
+ resolveTaskId(localId: number): Promise<number>;
14
+ updateTask(id: number, fields: Record<string, unknown>): Promise<Record<string, unknown>>;
15
+ getTaskNotes(taskId: number): Promise<Record<string, unknown>>;
16
+ addTaskNote(taskId: number, note: string, isPublic?: boolean): Promise<Record<string, unknown>>;
17
+ getProject(id: number): Promise<Record<string, unknown>>;
18
+ getMilestone(id: number): Promise<Record<string, unknown>>;
19
+ getTaskStatuses(): Promise<Record<string, unknown>>;
20
+ getTaskPriorities(): Promise<Record<string, unknown>>;
21
+ }
package/dist/client.js ADDED
@@ -0,0 +1,85 @@
1
+ export class IntervalsClient {
2
+ baseUrl = "https://api.myintervals.com";
3
+ authHeader;
4
+ constructor(apiToken) {
5
+ this.authHeader =
6
+ "Basic " + Buffer.from(`${apiToken}:X`).toString("base64");
7
+ }
8
+ async request(path, options = {}) {
9
+ const { method = "GET", body, params } = options;
10
+ let url = `${this.baseUrl}${path}`;
11
+ if (params) {
12
+ const searchParams = new URLSearchParams();
13
+ for (const [key, value] of Object.entries(params)) {
14
+ searchParams.set(key, String(value));
15
+ }
16
+ url += `?${searchParams.toString()}`;
17
+ }
18
+ const headers = {
19
+ Authorization: this.authHeader,
20
+ Accept: "application/json",
21
+ };
22
+ const fetchOptions = { method, headers };
23
+ if (body) {
24
+ headers["Content-Type"] = "application/json";
25
+ fetchOptions.body = JSON.stringify(body);
26
+ }
27
+ const response = await fetch(url, fetchOptions);
28
+ if (!response.ok) {
29
+ const text = await response.text();
30
+ throw new Error(`Intervals API error ${response.status}: ${text}`);
31
+ }
32
+ return response.json();
33
+ }
34
+ // --- Task ---
35
+ async getTask(id) {
36
+ const data = await this.request(`/task/${id}/`);
37
+ return data;
38
+ }
39
+ async getTaskByLocalId(localId) {
40
+ const data = await this.request(`/task/`, { params: { localid: localId } });
41
+ if (!data.task || data.task.length === 0) {
42
+ throw new Error(`No task found with local ID ${localId}`);
43
+ }
44
+ return data.task[0];
45
+ }
46
+ async resolveTaskId(localId) {
47
+ const task = await this.getTaskByLocalId(localId);
48
+ return Number(task.id);
49
+ }
50
+ async updateTask(id, fields) {
51
+ const data = await this.request(`/task/${id}/`, { method: "PUT", body: fields });
52
+ return data;
53
+ }
54
+ // --- Task Notes ---
55
+ async getTaskNotes(taskId) {
56
+ const data = await this.request(`/tasknote/`, { params: { taskid: taskId } });
57
+ return data;
58
+ }
59
+ async addTaskNote(taskId, note, isPublic = true) {
60
+ const data = await this.request(`/tasknote/`, {
61
+ method: "POST",
62
+ body: { taskid: taskId, note, public: isPublic },
63
+ });
64
+ return data;
65
+ }
66
+ // --- Project ---
67
+ async getProject(id) {
68
+ const data = await this.request(`/project/${id}/`);
69
+ return data;
70
+ }
71
+ // --- Milestone ---
72
+ async getMilestone(id) {
73
+ const data = await this.request(`/milestone/${id}/`);
74
+ return data;
75
+ }
76
+ // --- Resources (statuses & priorities) ---
77
+ async getTaskStatuses() {
78
+ const data = await this.request(`/taskstatus/`);
79
+ return data;
80
+ }
81
+ async getTaskPriorities() {
82
+ const data = await this.request(`/taskpriority/`);
83
+ return data;
84
+ }
85
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { IntervalsClient } from "./client.js";
5
+ import { registerTools } from "./tools.js";
6
+ import { registerResources } from "./resources.js";
7
+ const API_TOKEN = process.env.INTERVALS_API_TOKEN;
8
+ if (!API_TOKEN) {
9
+ console.error("Error: INTERVALS_API_TOKEN environment variable is required.");
10
+ process.exit(1);
11
+ }
12
+ const client = new IntervalsClient(API_TOKEN);
13
+ const server = new McpServer({
14
+ name: "mcp-intervals",
15
+ version: "1.0.0",
16
+ });
17
+ registerTools(server, client);
18
+ registerResources(server, client);
19
+ async function main() {
20
+ const transport = new StdioServerTransport();
21
+ await server.connect(transport);
22
+ }
23
+ main().catch((error) => {
24
+ console.error("Fatal error:", error);
25
+ process.exit(1);
26
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { IntervalsClient } from "./client.js";
3
+ export declare function registerResources(server: McpServer, client: IntervalsClient): void;
@@ -0,0 +1,34 @@
1
+ export function registerResources(server, client) {
2
+ // --- Task Statuses ---
3
+ server.resource("task-statuses", "intervals://statuses", {
4
+ description: "List of all task statuses with their IDs. Use these IDs when updating a task's status.",
5
+ mimeType: "application/json",
6
+ }, async () => {
7
+ const data = await client.getTaskStatuses();
8
+ return {
9
+ contents: [
10
+ {
11
+ uri: "intervals://statuses",
12
+ mimeType: "application/json",
13
+ text: JSON.stringify(data, null, 2),
14
+ },
15
+ ],
16
+ };
17
+ });
18
+ // --- Task Priorities ---
19
+ server.resource("task-priorities", "intervals://priorities", {
20
+ description: "List of all task priorities with their IDs. Use these IDs when updating a task's priority.",
21
+ mimeType: "application/json",
22
+ }, async () => {
23
+ const data = await client.getTaskPriorities();
24
+ return {
25
+ contents: [
26
+ {
27
+ uri: "intervals://priorities",
28
+ mimeType: "application/json",
29
+ text: JSON.stringify(data, null, 2),
30
+ },
31
+ ],
32
+ };
33
+ });
34
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { IntervalsClient } from "./client.js";
3
+ export declare function registerTools(server: McpServer, client: IntervalsClient): void;
package/dist/tools.js ADDED
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+ import { parseTaskIdFromUrl } from "./utils.js";
3
+ export function registerTools(server, client) {
4
+ // --- get_task ---
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).", {
6
+ task: z
7
+ .string()
8
+ .describe("Intervals task URL or numeric local task ID (as shown in the web UI)"),
9
+ }, async ({ task }) => {
10
+ const localId = parseTaskIdFromUrl(task);
11
+ const data = await client.getTaskByLocalId(localId);
12
+ return {
13
+ content: [
14
+ { type: "text", text: JSON.stringify(data, null, 2) },
15
+ ],
16
+ };
17
+ });
18
+ // --- update_task ---
19
+ server.tool("update_task", "Update fields on an Intervals task (status, assignee, priority, title, due date, owner).", {
20
+ taskId: z.number().describe("The local task ID (as shown in the Intervals web UI)"),
21
+ statusid: z
22
+ .number()
23
+ .optional()
24
+ .describe("New status ID (use intervals://statuses resource for valid IDs)"),
25
+ assigneeid: z
26
+ .number()
27
+ .optional()
28
+ .describe("New assignee person ID"),
29
+ priorityid: z
30
+ .number()
31
+ .optional()
32
+ .describe("New priority ID (use intervals://priorities resource for valid IDs)"),
33
+ title: z.string().optional().describe("New task title"),
34
+ datedue: z
35
+ .string()
36
+ .optional()
37
+ .describe("New due date in YYYY-MM-DD format"),
38
+ ownerid: z
39
+ .number()
40
+ .optional()
41
+ .describe("New owner person ID"),
42
+ }, async ({ taskId, ...fields }) => {
43
+ // Remove undefined fields
44
+ const updateData = {};
45
+ for (const [key, value] of Object.entries(fields)) {
46
+ if (value !== undefined) {
47
+ updateData[key] = value;
48
+ }
49
+ }
50
+ if (Object.keys(updateData).length === 0) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: "No fields provided to update.",
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ const internalId = await client.resolveTaskId(taskId);
61
+ const data = await client.updateTask(internalId, updateData);
62
+ return {
63
+ content: [
64
+ { type: "text", text: JSON.stringify(data, null, 2) },
65
+ ],
66
+ };
67
+ });
68
+ // --- add_task_note ---
69
+ server.tool("add_task_note", "Add a comment/note to an Intervals task.", {
70
+ taskId: z.number().describe("The local task ID (as shown in the Intervals web UI)"),
71
+ note: z
72
+ .string()
73
+ .describe("The note content (HTML is accepted)"),
74
+ isPublic: z
75
+ .boolean()
76
+ .default(true)
77
+ .describe("Whether the note is visible to executive users (defaults to true)"),
78
+ }, async ({ taskId, note, isPublic }) => {
79
+ const internalId = await client.resolveTaskId(taskId);
80
+ const data = await client.addTaskNote(internalId, note, isPublic);
81
+ return {
82
+ content: [
83
+ { type: "text", text: JSON.stringify(data, null, 2) },
84
+ ],
85
+ };
86
+ });
87
+ // --- get_task_notes ---
88
+ server.tool("get_task_notes", "Retrieve all comments/notes on an Intervals task.", {
89
+ taskId: z
90
+ .number()
91
+ .describe("The local task ID (as shown in the Intervals web UI)"),
92
+ }, async ({ taskId }) => {
93
+ const internalId = await client.resolveTaskId(taskId);
94
+ const data = await client.getTaskNotes(internalId);
95
+ return {
96
+ content: [
97
+ { type: "text", text: JSON.stringify(data, null, 2) },
98
+ ],
99
+ };
100
+ });
101
+ // --- get_project ---
102
+ server.tool("get_project", "Retrieve details of an Intervals project (name, client, dates, budget, description).", {
103
+ projectId: z
104
+ .number()
105
+ .describe("The numeric project ID"),
106
+ }, async ({ projectId }) => {
107
+ const data = await client.getProject(projectId);
108
+ return {
109
+ content: [
110
+ { type: "text", text: JSON.stringify(data, null, 2) },
111
+ ],
112
+ };
113
+ });
114
+ // --- get_milestone ---
115
+ server.tool("get_milestone", "Retrieve details of an Intervals milestone (title, due date, progress, description).", {
116
+ milestoneId: z
117
+ .number()
118
+ .describe("The numeric milestone ID"),
119
+ }, async ({ milestoneId }) => {
120
+ const data = await client.getMilestone(milestoneId);
121
+ return {
122
+ content: [
123
+ { type: "text", text: JSON.stringify(data, null, 2) },
124
+ ],
125
+ };
126
+ });
127
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Parse an Intervals task URL to extract the task ID.
3
+ * Accepts URLs like: https://<subdomain>.intervalsonline.com/tasks/view/<taskId>
4
+ * Also accepts a raw numeric string/number as fallback.
5
+ */
6
+ export declare function parseTaskIdFromUrl(input: string): number;
package/dist/utils.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Parse an Intervals task URL to extract the task ID.
3
+ * Accepts URLs like: https://<subdomain>.intervalsonline.com/tasks/view/<taskId>
4
+ * Also accepts a raw numeric string/number as fallback.
5
+ */
6
+ export function parseTaskIdFromUrl(input) {
7
+ const match = input.match(/\/tasks\/view\/(\d+)/);
8
+ if (match) {
9
+ return parseInt(match[1], 10);
10
+ }
11
+ const asNumber = parseInt(input, 10);
12
+ if (!isNaN(asNumber) && asNumber > 0) {
13
+ return asNumber;
14
+ }
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
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "mcp-intervals",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Intervals task management",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-intervals": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build",
16
+ "dev": "tsx src/index.ts",
17
+ "start": "node dist/index.js"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "mcp-server",
22
+ "intervals",
23
+ "task-management"
24
+ ],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/educlopez/mcp-intervals.git"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.12.1",
32
+ "zod": "^3.24.2"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.13.1",
36
+ "tsx": "^4.19.2",
37
+ "typescript": "^5.7.3"
38
+ }
39
+ }