mcp-intervals 1.0.2 → 1.1.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/LICENSE +21 -0
- package/README.md +35 -13
- package/dist/cli/api.d.ts +6 -0
- package/dist/cli/api.js +28 -0
- package/dist/cli/clients.d.ts +21 -0
- package/dist/cli/clients.js +136 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +173 -0
- package/dist/cli/prompts/search-multiselect.d.ts +14 -0
- package/dist/cli/prompts/search-multiselect.js +190 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +33 -0
- package/dist/index.js +26 -21
- package/dist/resources.js +16 -0
- package/dist/tools.js +72 -1
- package/package.json +5 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edu Calvo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,7 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
MCP server for [Intervals](https://www.myintervals.com/) task management. Lets Claude read and update tasks, add notes, and browse projects and milestones directly from your Intervals account.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
Run the interactive installer:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx mcp-intervals init
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This will:
|
|
17
|
+
1. Detect installed MCP clients (Claude Code, Claude Desktop, Cursor, Windsurf)
|
|
18
|
+
2. Let you select which clients to configure
|
|
19
|
+
3. Prompt for your Intervals API token
|
|
20
|
+
4. Validate the token with your Intervals account
|
|
21
|
+
5. Save the configuration automatically
|
|
22
|
+
|
|
23
|
+
## Manual Setup
|
|
9
24
|
|
|
10
25
|
### 1. Get your Intervals API token
|
|
11
26
|
|
|
@@ -93,21 +108,24 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
93
108
|
|
|
94
109
|
## Available Tools
|
|
95
110
|
|
|
96
|
-
| Tool
|
|
97
|
-
|
|
|
98
|
-
| `get_task`
|
|
99
|
-
| `update_task`
|
|
100
|
-
| `add_task_note`
|
|
101
|
-
| `get_task_notes`
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
| ------------------ | ---------------------------------------------------------------------------- |
|
|
113
|
+
| `get_task` | Get task details by local ID or Intervals URL |
|
|
114
|
+
| `update_task` | Update task status, assignee, priority, title, description, due date, owner |
|
|
115
|
+
| `add_task_note` | Add a comment/note to a task (supports HTML) |
|
|
116
|
+
| `get_task_notes` | Retrieve all comments/notes on a task |
|
|
117
|
+
| `add_time_entry` | Add a time entry to a task (billable/unbillable with work type) |
|
|
118
|
+
| `get_time_entries` | Retrieve time entries (filter by task, date range) |
|
|
119
|
+
| `get_project` | Get project details (name, client, dates, budget) |
|
|
120
|
+
| `get_milestone` | Get milestone details (title, due date, progress) |
|
|
104
121
|
|
|
105
122
|
## Resources
|
|
106
123
|
|
|
107
|
-
| Resource | URI | Description
|
|
108
|
-
| --------------- | ------------------------ |
|
|
109
|
-
| Task Statuses | `intervals://statuses` | List of all status IDs for use with `update_task`
|
|
110
|
-
| Task Priorities | `intervals://priorities` | List of all priority IDs for use with `update_task`
|
|
124
|
+
| Resource | URI | Description |
|
|
125
|
+
| --------------- | ------------------------ | ----------------------------------------------------- |
|
|
126
|
+
| Task Statuses | `intervals://statuses` | List of all status IDs for use with `update_task` |
|
|
127
|
+
| Task Priorities | `intervals://priorities` | List of all priority IDs for use with `update_task` |
|
|
128
|
+
| Work Types | `intervals://worktypes` | List of all work type IDs for use with time entries |
|
|
111
129
|
|
|
112
130
|
## Example Usage
|
|
113
131
|
|
|
@@ -115,8 +133,12 @@ Once installed, you can ask Claude things like:
|
|
|
115
133
|
|
|
116
134
|
- "Get the details of task 1234"
|
|
117
135
|
- "Update task 1234 status to closed"
|
|
136
|
+
- "Update the description of task 1234 to explain the new requirements"
|
|
118
137
|
- "Add a note to task 1234 saying the fix has been deployed"
|
|
119
138
|
- "Show me all notes on task 1234"
|
|
139
|
+
- "Log 2 hours of billable time on task 1234 for today"
|
|
140
|
+
- "Add 30 minutes of unbillable time to task 1234 for code review"
|
|
141
|
+
- "Show me all time entries for task 1234"
|
|
120
142
|
- "What are the details of project 5?"
|
|
121
143
|
|
|
122
144
|
## License
|
package/dist/cli/api.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function validateToken(token) {
|
|
2
|
+
try {
|
|
3
|
+
const authHeader = "Basic " + Buffer.from(`${token}:X`).toString("base64");
|
|
4
|
+
const response = await fetch("https://api.myintervals.com/me/", {
|
|
5
|
+
headers: {
|
|
6
|
+
Authorization: authHeader,
|
|
7
|
+
Accept: "application/json",
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (response.ok) {
|
|
11
|
+
const data = (await response.json());
|
|
12
|
+
return {
|
|
13
|
+
valid: true,
|
|
14
|
+
workspace: data.me?.company || "Unknown workspace",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 401) {
|
|
18
|
+
return { valid: false, error: "Invalid API token" };
|
|
19
|
+
}
|
|
20
|
+
return { valid: false, error: `API error: ${response.status}` };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof Error && error.message.includes("fetch")) {
|
|
24
|
+
return { valid: false, error: "Network error - could not reach Intervals API" };
|
|
25
|
+
}
|
|
26
|
+
return { valid: false, error: String(error) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface McpClient {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
detected: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface McpConfig {
|
|
8
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
interface McpServerConfig {
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export declare function detectClients(): McpClient[];
|
|
17
|
+
export declare function readConfig(configPath: string): McpConfig;
|
|
18
|
+
export declare function writeConfig(configPath: string, config: McpConfig): void;
|
|
19
|
+
export declare function configureClient(configPath: string, token: string): void;
|
|
20
|
+
export declare function hasExistingConfig(configPath: string): boolean;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
function getClientPaths() {
|
|
5
|
+
const platform = os.platform();
|
|
6
|
+
const home = os.homedir();
|
|
7
|
+
const clients = [];
|
|
8
|
+
// Claude Code (global)
|
|
9
|
+
clients.push({
|
|
10
|
+
id: "claude-code-global",
|
|
11
|
+
name: "Claude Code (global)",
|
|
12
|
+
path: path.join(home, ".claude.json"),
|
|
13
|
+
});
|
|
14
|
+
// Claude Code (project)
|
|
15
|
+
clients.push({
|
|
16
|
+
id: "claude-code-project",
|
|
17
|
+
name: "Claude Code (project)",
|
|
18
|
+
path: path.join(process.cwd(), ".mcp.json"),
|
|
19
|
+
});
|
|
20
|
+
// Claude Desktop
|
|
21
|
+
if (platform === "darwin") {
|
|
22
|
+
clients.push({
|
|
23
|
+
id: "claude-desktop",
|
|
24
|
+
name: "Claude Desktop",
|
|
25
|
+
path: path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else if (platform === "win32") {
|
|
29
|
+
clients.push({
|
|
30
|
+
id: "claude-desktop",
|
|
31
|
+
name: "Claude Desktop",
|
|
32
|
+
path: path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json"),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Cursor
|
|
36
|
+
if (platform === "darwin" || platform === "linux") {
|
|
37
|
+
clients.push({
|
|
38
|
+
id: "cursor",
|
|
39
|
+
name: "Cursor",
|
|
40
|
+
path: path.join(home, ".cursor", "mcp.json"),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (platform === "win32") {
|
|
44
|
+
clients.push({
|
|
45
|
+
id: "cursor",
|
|
46
|
+
name: "Cursor",
|
|
47
|
+
path: path.join(home, ".cursor", "mcp.json"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Windsurf
|
|
51
|
+
if (platform === "darwin" || platform === "linux") {
|
|
52
|
+
clients.push({
|
|
53
|
+
id: "windsurf",
|
|
54
|
+
name: "Windsurf",
|
|
55
|
+
path: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (platform === "win32") {
|
|
59
|
+
clients.push({
|
|
60
|
+
id: "windsurf",
|
|
61
|
+
name: "Windsurf",
|
|
62
|
+
path: path.join(process.env.APPDATA || "", "Codeium", "windsurf", "mcp_config.json"),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return clients;
|
|
66
|
+
}
|
|
67
|
+
export function detectClients() {
|
|
68
|
+
const clientPaths = getClientPaths();
|
|
69
|
+
return clientPaths.map((client) => {
|
|
70
|
+
let detected = false;
|
|
71
|
+
// Check if file exists OR if parent directory exists (we can create the file)
|
|
72
|
+
if (fs.existsSync(client.path)) {
|
|
73
|
+
detected = true;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const parentDir = path.dirname(client.path);
|
|
77
|
+
if (fs.existsSync(parentDir)) {
|
|
78
|
+
detected = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
id: client.id,
|
|
83
|
+
name: client.name,
|
|
84
|
+
configPath: client.path,
|
|
85
|
+
detected,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function readConfig(configPath) {
|
|
90
|
+
if (!fs.existsSync(configPath)) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
95
|
+
return JSON.parse(content);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error(`Invalid JSON in ${configPath}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function writeConfig(configPath, config) {
|
|
102
|
+
const dir = path.dirname(configPath);
|
|
103
|
+
// Create directory if it doesn't exist
|
|
104
|
+
if (!fs.existsSync(dir)) {
|
|
105
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
// Create backup if file exists
|
|
108
|
+
if (fs.existsSync(configPath)) {
|
|
109
|
+
const backupPath = configPath + ".bak";
|
|
110
|
+
fs.copyFileSync(configPath, backupPath);
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
113
|
+
}
|
|
114
|
+
export function configureClient(configPath, token) {
|
|
115
|
+
const config = readConfig(configPath);
|
|
116
|
+
if (!config.mcpServers) {
|
|
117
|
+
config.mcpServers = {};
|
|
118
|
+
}
|
|
119
|
+
config.mcpServers.intervals = {
|
|
120
|
+
command: "npx",
|
|
121
|
+
args: ["-y", "mcp-intervals"],
|
|
122
|
+
env: {
|
|
123
|
+
INTERVALS_API_TOKEN: token,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
writeConfig(configPath, config);
|
|
127
|
+
}
|
|
128
|
+
export function hasExistingConfig(configPath) {
|
|
129
|
+
try {
|
|
130
|
+
const config = readConfig(configPath);
|
|
131
|
+
return config.mcpServers?.intervals !== undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { detectClients, configureClient, hasExistingConfig } from "./clients.js";
|
|
5
|
+
import { validateToken } from "./api.js";
|
|
6
|
+
import { searchMultiselect, cancelSymbol } from "./prompts/search-multiselect.js";
|
|
7
|
+
// Logo ASCII art with gradient grays (256-color)
|
|
8
|
+
const LOGO_LINES = [
|
|
9
|
+
"██╗███╗ ██╗████████╗███████╗██████╗ ██╗ ██╗ █████╗ ██╗ ███████╗",
|
|
10
|
+
"██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗██║ ██║██╔══██╗██║ ██╔════╝",
|
|
11
|
+
"██║██╔██╗ ██║ ██║ █████╗ ██████╔╝██║ ██║███████║██║ ███████╗",
|
|
12
|
+
"██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══██║██║ ╚════██║",
|
|
13
|
+
"██║██║ ╚████║ ██║ ███████╗██║ ██║ ╚████╔╝ ██║ ██║███████╗███████║",
|
|
14
|
+
"╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚══════╝╚══════╝",
|
|
15
|
+
];
|
|
16
|
+
// 256-color grays for gradient effect
|
|
17
|
+
const GRAYS = [
|
|
18
|
+
"\x1b[38;5;250m", // lighter
|
|
19
|
+
"\x1b[38;5;248m",
|
|
20
|
+
"\x1b[38;5;245m",
|
|
21
|
+
"\x1b[38;5;243m",
|
|
22
|
+
"\x1b[38;5;240m",
|
|
23
|
+
"\x1b[38;5;238m", // darker
|
|
24
|
+
];
|
|
25
|
+
const RESET = "\x1b[0m";
|
|
26
|
+
function showLogo() {
|
|
27
|
+
console.log();
|
|
28
|
+
LOGO_LINES.forEach((line, i) => {
|
|
29
|
+
console.log(`${GRAYS[i]}${line}${RESET}`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function shortenPath(fullPath) {
|
|
33
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
34
|
+
if (home && fullPath.startsWith(home)) {
|
|
35
|
+
return "~" + fullPath.slice(home.length);
|
|
36
|
+
}
|
|
37
|
+
return fullPath;
|
|
38
|
+
}
|
|
39
|
+
async function main() {
|
|
40
|
+
showLogo();
|
|
41
|
+
console.log();
|
|
42
|
+
p.intro(pc.bgCyan(pc.black(" intervals ")));
|
|
43
|
+
const spinner = p.spinner();
|
|
44
|
+
// Detect clients
|
|
45
|
+
spinner.start("Detecting MCP clients...");
|
|
46
|
+
const clients = detectClients();
|
|
47
|
+
const detectedClients = clients.filter((c) => c.detected);
|
|
48
|
+
const notFoundClients = clients.filter((c) => !c.detected);
|
|
49
|
+
spinner.stop(`${detectedClients.length} clients found`);
|
|
50
|
+
// Show detection results
|
|
51
|
+
console.log();
|
|
52
|
+
p.log.message(pc.bold("Found installed clients:"));
|
|
53
|
+
for (const client of detectedClients) {
|
|
54
|
+
p.log.message(` ${pc.green("✓")} ${client.name}`);
|
|
55
|
+
}
|
|
56
|
+
for (const client of notFoundClients) {
|
|
57
|
+
p.log.message(` ${pc.dim("✗")} ${pc.dim(client.name + " (not found)")}`);
|
|
58
|
+
}
|
|
59
|
+
if (detectedClients.length === 0) {
|
|
60
|
+
p.cancel("No MCP clients detected. Install Claude Code, Claude Desktop, Cursor, or Windsurf first.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// Select clients with search multiselect
|
|
64
|
+
const clientChoices = detectedClients.map((client) => {
|
|
65
|
+
const hasExisting = hasExistingConfig(client.configPath);
|
|
66
|
+
return {
|
|
67
|
+
value: client.id,
|
|
68
|
+
label: client.name,
|
|
69
|
+
hint: shortenPath(client.configPath) + (hasExisting ? " - will overwrite" : ""),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
// Pre-select clients that don't have existing config
|
|
73
|
+
const initialSelected = detectedClients
|
|
74
|
+
.filter((c) => !hasExistingConfig(c.configPath))
|
|
75
|
+
.map((c) => c.id);
|
|
76
|
+
console.log();
|
|
77
|
+
const selectedIds = await searchMultiselect({
|
|
78
|
+
message: "Which clients do you want to configure?",
|
|
79
|
+
items: clientChoices,
|
|
80
|
+
initialSelected: initialSelected.length > 0 ? initialSelected : [detectedClients[0]?.id].filter(Boolean),
|
|
81
|
+
required: true,
|
|
82
|
+
});
|
|
83
|
+
if (selectedIds === cancelSymbol || (Array.isArray(selectedIds) && selectedIds.length === 0)) {
|
|
84
|
+
p.cancel("Installation cancelled");
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
const selectedClients = detectedClients.filter((c) => Array.isArray(selectedIds) && selectedIds.includes(c.id));
|
|
88
|
+
// Get API token
|
|
89
|
+
console.log();
|
|
90
|
+
const token = await p.password({
|
|
91
|
+
message: "Enter your Intervals API token:",
|
|
92
|
+
});
|
|
93
|
+
if (p.isCancel(token) || !token || token.trim() === "") {
|
|
94
|
+
p.cancel("No token provided");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
// Validate token
|
|
98
|
+
spinner.start("Validating token...");
|
|
99
|
+
const validation = await validateToken(token.trim());
|
|
100
|
+
if (!validation.valid) {
|
|
101
|
+
spinner.stop(pc.red("Token validation failed"));
|
|
102
|
+
p.log.error(validation.error || "Invalid token");
|
|
103
|
+
p.log.message(pc.dim(" Find your API token at: https://[subdomain].myintervals.com/account/api/"));
|
|
104
|
+
console.log();
|
|
105
|
+
const continueAnyway = await p.confirm({
|
|
106
|
+
message: "Save configuration anyway (without validation)?",
|
|
107
|
+
initialValue: false,
|
|
108
|
+
});
|
|
109
|
+
if (p.isCancel(continueAnyway) || !continueAnyway) {
|
|
110
|
+
p.cancel("Installation cancelled");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
spinner.stop(`Token valid! Connected to "${validation.workspace}"`);
|
|
116
|
+
}
|
|
117
|
+
// Show summary and confirm
|
|
118
|
+
console.log();
|
|
119
|
+
const summaryLines = [];
|
|
120
|
+
for (const client of selectedClients) {
|
|
121
|
+
summaryLines.push(`${pc.cyan(client.name)} ${pc.dim(shortenPath(client.configPath))}`);
|
|
122
|
+
}
|
|
123
|
+
p.note(summaryLines.join("\n"), "Will configure");
|
|
124
|
+
const confirmed = await p.confirm({
|
|
125
|
+
message: "Proceed with configuration?",
|
|
126
|
+
initialValue: true,
|
|
127
|
+
});
|
|
128
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
129
|
+
p.cancel("Installation cancelled");
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
// Configure each client
|
|
133
|
+
spinner.start("Saving configuration...");
|
|
134
|
+
const results = [];
|
|
135
|
+
for (const client of selectedClients) {
|
|
136
|
+
try {
|
|
137
|
+
configureClient(client.configPath, token.trim());
|
|
138
|
+
results.push({ client: client.name, success: true });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
results.push({
|
|
142
|
+
client: client.name,
|
|
143
|
+
success: false,
|
|
144
|
+
error: error instanceof Error ? error.message : String(error)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const successful = results.filter((r) => r.success);
|
|
149
|
+
const failed = results.filter((r) => !r.success);
|
|
150
|
+
spinner.stop("Configuration complete");
|
|
151
|
+
// Show results
|
|
152
|
+
console.log();
|
|
153
|
+
if (successful.length > 0) {
|
|
154
|
+
const resultLines = successful.map((r) => {
|
|
155
|
+
const client = selectedClients.find((c) => c.name === r.client);
|
|
156
|
+
return `${pc.green("✓")} ${r.client} ${pc.dim(client ? shortenPath(client.configPath) : "")}`;
|
|
157
|
+
});
|
|
158
|
+
p.note(resultLines.join("\n"), pc.green(`Configured ${successful.length} client${successful.length !== 1 ? "s" : ""}`));
|
|
159
|
+
}
|
|
160
|
+
if (failed.length > 0) {
|
|
161
|
+
console.log();
|
|
162
|
+
p.log.error(pc.red(`Failed to configure ${failed.length} client${failed.length !== 1 ? "s" : ""}:`));
|
|
163
|
+
for (const r of failed) {
|
|
164
|
+
p.log.message(` ${pc.red("✗")} ${r.client}: ${pc.dim(r.error)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.log();
|
|
168
|
+
p.outro(pc.green("Done! Restart your MCP clients to use mcp-intervals."));
|
|
169
|
+
}
|
|
170
|
+
main().catch((error) => {
|
|
171
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SearchItem<T> {
|
|
2
|
+
value: T;
|
|
3
|
+
label: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SearchMultiselectOptions<T> {
|
|
7
|
+
message: string;
|
|
8
|
+
items: SearchItem<T>[];
|
|
9
|
+
maxVisible?: number;
|
|
10
|
+
initialSelected?: T[];
|
|
11
|
+
required?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const cancelSymbol: unique symbol;
|
|
14
|
+
export declare function searchMultiselect<T>(options: SearchMultiselectOptions<T>): Promise<T[] | symbol>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import { Writable } from "stream";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
// Silent writable stream to prevent readline from echoing input
|
|
5
|
+
const silentOutput = new Writable({
|
|
6
|
+
write(_chunk, _encoding, callback) {
|
|
7
|
+
callback();
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
const S_STEP_ACTIVE = pc.green("◆");
|
|
11
|
+
const S_STEP_CANCEL = pc.red("■");
|
|
12
|
+
const S_STEP_SUBMIT = pc.green("◇");
|
|
13
|
+
const S_RADIO_ACTIVE = pc.green("●");
|
|
14
|
+
const S_RADIO_INACTIVE = pc.dim("○");
|
|
15
|
+
const S_BAR = pc.dim("│");
|
|
16
|
+
export const cancelSymbol = Symbol("cancel");
|
|
17
|
+
export async function searchMultiselect(options) {
|
|
18
|
+
const { message, items, maxVisible = 8, initialSelected = [], required = false } = options;
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const rl = readline.createInterface({
|
|
21
|
+
input: process.stdin,
|
|
22
|
+
output: silentOutput,
|
|
23
|
+
terminal: false,
|
|
24
|
+
});
|
|
25
|
+
if (process.stdin.isTTY) {
|
|
26
|
+
process.stdin.setRawMode(true);
|
|
27
|
+
}
|
|
28
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
29
|
+
let query = "";
|
|
30
|
+
let cursor = 0;
|
|
31
|
+
const selected = new Set(initialSelected);
|
|
32
|
+
let lastRenderHeight = 0;
|
|
33
|
+
const filter = (item, q) => {
|
|
34
|
+
if (!q)
|
|
35
|
+
return true;
|
|
36
|
+
const lowerQ = q.toLowerCase();
|
|
37
|
+
return (item.label.toLowerCase().includes(lowerQ) ||
|
|
38
|
+
String(item.value).toLowerCase().includes(lowerQ));
|
|
39
|
+
};
|
|
40
|
+
const getFiltered = () => {
|
|
41
|
+
return items.filter((item) => filter(item, query));
|
|
42
|
+
};
|
|
43
|
+
const clearRender = () => {
|
|
44
|
+
if (lastRenderHeight > 0) {
|
|
45
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
46
|
+
for (let i = 0; i < lastRenderHeight; i++) {
|
|
47
|
+
process.stdout.write("\x1b[2K\x1b[1B");
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const render = (state = "active") => {
|
|
53
|
+
clearRender();
|
|
54
|
+
const lines = [];
|
|
55
|
+
const filtered = getFiltered();
|
|
56
|
+
const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
|
|
57
|
+
lines.push(`${icon} ${pc.bold(message)}`);
|
|
58
|
+
if (state === "active") {
|
|
59
|
+
const searchLine = `${S_BAR} ${pc.dim("Search:")} ${query}${pc.inverse(" ")}`;
|
|
60
|
+
lines.push(searchLine);
|
|
61
|
+
lines.push(`${S_BAR} ${pc.dim("↑↓ move, space select, enter confirm")}`);
|
|
62
|
+
lines.push(`${S_BAR}`);
|
|
63
|
+
const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
64
|
+
const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
|
|
65
|
+
const visibleItems = filtered.slice(visibleStart, visibleEnd);
|
|
66
|
+
if (filtered.length === 0) {
|
|
67
|
+
lines.push(`${S_BAR} ${pc.dim("No matches found")}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
71
|
+
const item = visibleItems[i];
|
|
72
|
+
const actualIndex = visibleStart + i;
|
|
73
|
+
const isSelected = selected.has(item.value);
|
|
74
|
+
const isCursor = actualIndex === cursor;
|
|
75
|
+
const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
|
|
76
|
+
const label = isCursor ? pc.underline(item.label) : item.label;
|
|
77
|
+
const hint = item.hint ? pc.dim(` (${item.hint})`) : "";
|
|
78
|
+
const prefix = isCursor ? pc.cyan("❯") : " ";
|
|
79
|
+
lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
|
|
80
|
+
}
|
|
81
|
+
const hiddenBefore = visibleStart;
|
|
82
|
+
const hiddenAfter = filtered.length - visibleEnd;
|
|
83
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (hiddenBefore > 0)
|
|
86
|
+
parts.push(`↑ ${hiddenBefore} more`);
|
|
87
|
+
if (hiddenAfter > 0)
|
|
88
|
+
parts.push(`↓ ${hiddenAfter} more`);
|
|
89
|
+
lines.push(`${S_BAR} ${pc.dim(parts.join(" "))}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push(`${S_BAR}`);
|
|
93
|
+
if (selected.size === 0) {
|
|
94
|
+
lines.push(`${S_BAR} ${pc.dim("Selected: (none)")}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const selectedLabels = items
|
|
98
|
+
.filter((item) => selected.has(item.value))
|
|
99
|
+
.map((item) => item.label);
|
|
100
|
+
const summary = selectedLabels.length <= 3
|
|
101
|
+
? selectedLabels.join(", ")
|
|
102
|
+
: `${selectedLabels.slice(0, 3).join(", ")} +${selectedLabels.length - 3} more`;
|
|
103
|
+
lines.push(`${S_BAR} ${pc.green("Selected:")} ${summary}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push(`${pc.dim("└")}`);
|
|
106
|
+
}
|
|
107
|
+
else if (state === "submit") {
|
|
108
|
+
const selectedLabels = items
|
|
109
|
+
.filter((item) => selected.has(item.value))
|
|
110
|
+
.map((item) => item.label);
|
|
111
|
+
lines.push(`${S_BAR} ${pc.dim(selectedLabels.join(", "))}`);
|
|
112
|
+
}
|
|
113
|
+
else if (state === "cancel") {
|
|
114
|
+
lines.push(`${S_BAR} ${pc.strikethrough(pc.dim("Cancelled"))}`);
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
117
|
+
lastRenderHeight = lines.length;
|
|
118
|
+
};
|
|
119
|
+
const cleanup = () => {
|
|
120
|
+
process.stdin.removeListener("keypress", keypressHandler);
|
|
121
|
+
if (process.stdin.isTTY) {
|
|
122
|
+
process.stdin.setRawMode(false);
|
|
123
|
+
}
|
|
124
|
+
rl.close();
|
|
125
|
+
};
|
|
126
|
+
const submit = () => {
|
|
127
|
+
if (required && selected.size === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
render("submit");
|
|
131
|
+
cleanup();
|
|
132
|
+
resolve(Array.from(selected));
|
|
133
|
+
};
|
|
134
|
+
const cancel = () => {
|
|
135
|
+
render("cancel");
|
|
136
|
+
cleanup();
|
|
137
|
+
resolve(cancelSymbol);
|
|
138
|
+
};
|
|
139
|
+
const keypressHandler = (_str, key) => {
|
|
140
|
+
if (!key)
|
|
141
|
+
return;
|
|
142
|
+
const filtered = getFiltered();
|
|
143
|
+
if (key.name === "return") {
|
|
144
|
+
submit();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
148
|
+
cancel();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (key.name === "up") {
|
|
152
|
+
cursor = Math.max(0, cursor - 1);
|
|
153
|
+
render();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (key.name === "down") {
|
|
157
|
+
cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
158
|
+
render();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (key.name === "space") {
|
|
162
|
+
const item = filtered[cursor];
|
|
163
|
+
if (item) {
|
|
164
|
+
if (selected.has(item.value)) {
|
|
165
|
+
selected.delete(item.value);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
selected.add(item.value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
render();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (key.name === "backspace") {
|
|
175
|
+
query = query.slice(0, -1);
|
|
176
|
+
cursor = 0;
|
|
177
|
+
render();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
181
|
+
query += key.sequence;
|
|
182
|
+
cursor = 0;
|
|
183
|
+
render();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
process.stdin.on("keypress", keypressHandler);
|
|
188
|
+
render();
|
|
189
|
+
});
|
|
190
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -18,4 +18,20 @@ export declare class IntervalsClient {
|
|
|
18
18
|
getMilestone(id: number): Promise<Record<string, unknown>>;
|
|
19
19
|
getTaskStatuses(): Promise<Record<string, unknown>>;
|
|
20
20
|
getTaskPriorities(): Promise<Record<string, unknown>>;
|
|
21
|
+
getWorkTypes(): Promise<Record<string, unknown>>;
|
|
22
|
+
addTimeEntry(fields: {
|
|
23
|
+
taskid: number;
|
|
24
|
+
worktypeid: number;
|
|
25
|
+
date: string;
|
|
26
|
+
time: number;
|
|
27
|
+
billable: boolean;
|
|
28
|
+
description?: string;
|
|
29
|
+
}): Promise<Record<string, unknown>>;
|
|
30
|
+
getTimeEntries(params?: {
|
|
31
|
+
taskid?: number;
|
|
32
|
+
personid?: number;
|
|
33
|
+
datebegin?: string;
|
|
34
|
+
dateend?: string;
|
|
35
|
+
}): Promise<Record<string, unknown>>;
|
|
36
|
+
getMe(): Promise<Record<string, unknown>>;
|
|
21
37
|
}
|
package/dist/client.js
CHANGED
|
@@ -82,4 +82,37 @@ export class IntervalsClient {
|
|
|
82
82
|
const data = await this.request(`/taskpriority/`);
|
|
83
83
|
return data;
|
|
84
84
|
}
|
|
85
|
+
// --- Work Types ---
|
|
86
|
+
async getWorkTypes() {
|
|
87
|
+
const data = await this.request(`/worktype/`);
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
// --- Time Entries ---
|
|
91
|
+
async addTimeEntry(fields) {
|
|
92
|
+
// First get the current user's person ID
|
|
93
|
+
const me = await this.getMe();
|
|
94
|
+
const personid = me.personid;
|
|
95
|
+
const data = await this.request(`/time/`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
body: {
|
|
98
|
+
taskid: fields.taskid,
|
|
99
|
+
worktypeid: fields.worktypeid,
|
|
100
|
+
personid,
|
|
101
|
+
date: fields.date,
|
|
102
|
+
time: fields.time,
|
|
103
|
+
billable: fields.billable,
|
|
104
|
+
...(fields.description && { description: fields.description }),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
async getTimeEntries(params = {}) {
|
|
110
|
+
const data = await this.request(`/time/`, { params: params });
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
// --- Me (current user) ---
|
|
114
|
+
async getMe() {
|
|
115
|
+
const data = await this.request(`/me/`);
|
|
116
|
+
return data.me;
|
|
117
|
+
}
|
|
85
118
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
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);
|
|
2
|
+
// Handle CLI subcommands
|
|
3
|
+
if (process.argv[2] === "init") {
|
|
4
|
+
import("./cli/init.js");
|
|
11
5
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
else {
|
|
7
|
+
// MCP Server mode
|
|
8
|
+
startServer();
|
|
9
|
+
}
|
|
10
|
+
async function startServer() {
|
|
11
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
12
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
|
+
const { IntervalsClient } = await import("./client.js");
|
|
14
|
+
const { registerTools } = await import("./tools.js");
|
|
15
|
+
const { registerResources } = await import("./resources.js");
|
|
16
|
+
const API_TOKEN = process.env.INTERVALS_API_TOKEN;
|
|
17
|
+
if (!API_TOKEN) {
|
|
18
|
+
console.error("Error: INTERVALS_API_TOKEN environment variable is required.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const client = new IntervalsClient(API_TOKEN);
|
|
22
|
+
const server = new McpServer({
|
|
23
|
+
name: "mcp-intervals",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
});
|
|
26
|
+
registerTools(server, client);
|
|
27
|
+
registerResources(server, client);
|
|
20
28
|
const transport = new StdioServerTransport();
|
|
21
29
|
await server.connect(transport);
|
|
22
30
|
}
|
|
23
|
-
|
|
24
|
-
console.error("Fatal error:", error);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
});
|
|
31
|
+
export {};
|
package/dist/resources.js
CHANGED
|
@@ -31,4 +31,20 @@ export function registerResources(server, client) {
|
|
|
31
31
|
],
|
|
32
32
|
};
|
|
33
33
|
});
|
|
34
|
+
// --- Work Types ---
|
|
35
|
+
server.resource("work-types", "intervals://worktypes", {
|
|
36
|
+
description: "List of all work types with their IDs. Use these IDs when adding a time entry.",
|
|
37
|
+
mimeType: "application/json",
|
|
38
|
+
}, async () => {
|
|
39
|
+
const data = await client.getWorkTypes();
|
|
40
|
+
return {
|
|
41
|
+
contents: [
|
|
42
|
+
{
|
|
43
|
+
uri: "intervals://worktypes",
|
|
44
|
+
mimeType: "application/json",
|
|
45
|
+
text: JSON.stringify(data, null, 2),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
});
|
|
34
50
|
}
|
package/dist/tools.js
CHANGED
|
@@ -16,7 +16,7 @@ export function registerTools(server, client) {
|
|
|
16
16
|
};
|
|
17
17
|
});
|
|
18
18
|
// --- update_task ---
|
|
19
|
-
server.tool("update_task", "Update fields on an Intervals task (status, assignee, priority, title, due date, owner).", {
|
|
19
|
+
server.tool("update_task", "Update fields on an Intervals task (status, assignee, priority, title, description, due date, owner).", {
|
|
20
20
|
taskId: z.number().describe("The local task ID (as shown in the Intervals web UI)"),
|
|
21
21
|
statusid: z
|
|
22
22
|
.number()
|
|
@@ -31,6 +31,10 @@ export function registerTools(server, client) {
|
|
|
31
31
|
.optional()
|
|
32
32
|
.describe("New priority ID (use intervals://priorities resource for valid IDs)"),
|
|
33
33
|
title: z.string().optional().describe("New task title"),
|
|
34
|
+
summary: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("New task description/summary (HTML is accepted)"),
|
|
34
38
|
datedue: z
|
|
35
39
|
.string()
|
|
36
40
|
.optional()
|
|
@@ -124,4 +128,71 @@ export function registerTools(server, client) {
|
|
|
124
128
|
],
|
|
125
129
|
};
|
|
126
130
|
});
|
|
131
|
+
// --- add_time_entry ---
|
|
132
|
+
server.tool("add_time_entry", "Add a time entry to an Intervals task. Records time worked as billable or unbillable with a specific work type.", {
|
|
133
|
+
taskId: z
|
|
134
|
+
.number()
|
|
135
|
+
.describe("The local task ID (as shown in the Intervals web UI)"),
|
|
136
|
+
worktypeid: z
|
|
137
|
+
.number()
|
|
138
|
+
.describe("Work type ID (use intervals://worktypes resource for valid IDs)"),
|
|
139
|
+
date: z
|
|
140
|
+
.string()
|
|
141
|
+
.describe("Date of the time entry in YYYY-MM-DD format"),
|
|
142
|
+
time: z
|
|
143
|
+
.number()
|
|
144
|
+
.describe("Time worked in decimal hours (e.g., 1.5 for 1 hour 30 minutes)"),
|
|
145
|
+
billable: z
|
|
146
|
+
.boolean()
|
|
147
|
+
.describe("Whether the time is billable (true) or unbillable (false)"),
|
|
148
|
+
description: z
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Optional description of work performed"),
|
|
152
|
+
}, async ({ taskId, worktypeid, date, time, billable, description }) => {
|
|
153
|
+
const internalId = await client.resolveTaskId(taskId);
|
|
154
|
+
const data = await client.addTimeEntry({
|
|
155
|
+
taskid: internalId,
|
|
156
|
+
worktypeid,
|
|
157
|
+
date,
|
|
158
|
+
time,
|
|
159
|
+
billable,
|
|
160
|
+
description,
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{ type: "text", text: JSON.stringify(data, null, 2) },
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
// --- get_time_entries ---
|
|
169
|
+
server.tool("get_time_entries", "Retrieve time entries from Intervals. Can filter by task, person, or date range.", {
|
|
170
|
+
taskId: z
|
|
171
|
+
.number()
|
|
172
|
+
.optional()
|
|
173
|
+
.describe("Filter by local task ID (as shown in the Intervals web UI)"),
|
|
174
|
+
datebegin: z
|
|
175
|
+
.string()
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("Start date filter in YYYY-MM-DD format"),
|
|
178
|
+
dateend: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("End date filter in YYYY-MM-DD format"),
|
|
182
|
+
}, async ({ taskId, datebegin, dateend }) => {
|
|
183
|
+
let internalTaskId;
|
|
184
|
+
if (taskId) {
|
|
185
|
+
internalTaskId = await client.resolveTaskId(taskId);
|
|
186
|
+
}
|
|
187
|
+
const data = await client.getTimeEntries({
|
|
188
|
+
taskid: internalTaskId,
|
|
189
|
+
datebegin,
|
|
190
|
+
dateend,
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{ type: "text", text: JSON.stringify(data, null, 2) },
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
});
|
|
127
198
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-intervals",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for Intervals task management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mcp-intervals": "dist/index.js"
|
|
8
|
+
"mcp-intervals": "dist/index.js",
|
|
9
|
+
"mcp-intervals-init": "dist/cli/init.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist"
|
|
@@ -28,7 +29,9 @@
|
|
|
28
29
|
"url": "https://github.com/educlopez/mcp-intervals.git"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^1.0.0",
|
|
31
33
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
34
|
+
"picocolors": "^1.1.1",
|
|
32
35
|
"zod": "^3.24.2"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|