@doist/todoist-ai 2.0.0 → 2.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/README.md +7 -0
- package/dist/index.d.ts +29 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -48
- package/dist/main.js +6 -11
- package/dist/mcp-helpers.d.ts +2 -2
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +1 -4
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +34 -36
- package/dist/todoist-tool.js +1 -2
- package/dist/tool-helpers.d.ts +13 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +43 -22
- package/dist/tool-helpers.test.js +55 -14
- package/dist/tools/__tests__/delete-one.test.d.ts +2 -0
- package/dist/tools/__tests__/delete-one.test.d.ts.map +1 -0
- package/dist/tools/__tests__/delete-one.test.js +90 -0
- package/dist/tools/__tests__/overview.test.d.ts +2 -0
- package/dist/tools/__tests__/overview.test.d.ts.map +1 -0
- package/dist/tools/__tests__/overview.test.js +163 -0
- package/dist/tools/__tests__/projects-list.test.d.ts +2 -0
- package/dist/tools/__tests__/projects-list.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects-list.test.js +140 -0
- package/dist/tools/__tests__/projects-manage.test.d.ts +2 -0
- package/dist/tools/__tests__/projects-manage.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects-manage.test.js +106 -0
- package/dist/tools/__tests__/sections-manage.test.d.ts +2 -0
- package/dist/tools/__tests__/sections-manage.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sections-manage.test.js +138 -0
- package/dist/tools/__tests__/sections-search.test.d.ts +2 -0
- package/dist/tools/__tests__/sections-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sections-search.test.js +235 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-add-multiple.test.js +274 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-complete-multiple.test.js +146 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-by-date.test.js +192 -0
- package/dist/tools/__tests__/tasks-list-completed.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-completed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-completed.test.js +154 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-list-for-container.test.js +232 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-organize-multiple.test.js +245 -0
- package/dist/tools/__tests__/tasks-search.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-search.test.js +106 -0
- package/dist/tools/__tests__/tasks-update-one.test.d.ts +2 -0
- package/dist/tools/__tests__/tasks-update-one.test.d.ts.map +1 -0
- package/dist/tools/__tests__/tasks-update-one.test.js +251 -0
- package/dist/tools/delete-one.js +4 -7
- package/dist/tools/overview.js +8 -11
- package/dist/tools/projects-list.js +7 -10
- package/dist/tools/projects-manage.js +6 -9
- package/dist/tools/sections-manage.js +7 -10
- package/dist/tools/sections-search.js +4 -7
- package/dist/tools/tasks-add-multiple.d.ts +5 -0
- package/dist/tools/tasks-add-multiple.d.ts.map +1 -1
- package/dist/tools/tasks-add-multiple.js +37 -17
- package/dist/tools/tasks-complete-multiple.js +3 -6
- package/dist/tools/tasks-list-by-date.d.ts +1 -0
- package/dist/tools/tasks-list-by-date.d.ts.map +1 -1
- package/dist/tools/tasks-list-by-date.js +12 -15
- package/dist/tools/tasks-list-completed.d.ts +2 -1
- package/dist/tools/tasks-list-completed.d.ts.map +1 -1
- package/dist/tools/tasks-list-completed.js +13 -16
- package/dist/tools/tasks-list-for-container.d.ts +1 -0
- package/dist/tools/tasks-list-for-container.d.ts.map +1 -1
- package/dist/tools/tasks-list-for-container.js +8 -11
- package/dist/tools/tasks-organize-multiple.d.ts.map +1 -1
- package/dist/tools/tasks-organize-multiple.js +20 -14
- package/dist/tools/tasks-search.d.ts +1 -0
- package/dist/tools/tasks-search.d.ts.map +1 -1
- package/dist/tools/tasks-search.js +7 -10
- package/dist/tools/tasks-update-one.d.ts +4 -2
- package/dist/tools/tasks-update-one.d.ts.map +1 -1
- package/dist/tools/tasks-update-one.js +45 -15
- package/dist/tools/test-helpers.d.ts +80 -0
- package/dist/tools/test-helpers.d.ts.map +1 -0
- package/dist/tools/test-helpers.js +140 -0
- package/dist/utils/duration-parser.d.ts +36 -0
- package/dist/utils/duration-parser.d.ts.map +1 -0
- package/dist/utils/duration-parser.js +96 -0
- package/dist/utils/duration-parser.test.d.ts +2 -0
- package/dist/utils/duration-parser.test.d.ts.map +1 -0
- package/dist/utils/duration-parser.test.js +147 -0
- package/package.json +6 -2
- package/scripts/test-executable.cjs +69 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration parser utility for converting human-readable duration strings
|
|
3
|
+
* to minutes using a restricted, language-neutral syntax.
|
|
4
|
+
*
|
|
5
|
+
* Supported formats:
|
|
6
|
+
* - "2h" (hours only)
|
|
7
|
+
* - "90m" (minutes only)
|
|
8
|
+
* - "2h30m" (hours + minutes)
|
|
9
|
+
* - "1.5h" (decimal hours)
|
|
10
|
+
* - Supports optional spaces: "2h 30m"
|
|
11
|
+
*/
|
|
12
|
+
export class DurationParseError extends Error {
|
|
13
|
+
constructor(input, reason) {
|
|
14
|
+
super(`Invalid duration format "${input}": ${reason}`);
|
|
15
|
+
this.name = 'DurationParseError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parses duration string in restricted syntax to minutes.
|
|
20
|
+
* Max duration: 1440 minutes (24 hours)
|
|
21
|
+
*
|
|
22
|
+
* @param durationStr - Duration string like "2h30m", "45m", "1.5h"
|
|
23
|
+
* @returns Parsed duration in minutes
|
|
24
|
+
* @throws DurationParseError for invalid formats
|
|
25
|
+
*/
|
|
26
|
+
export function parseDuration(durationStr) {
|
|
27
|
+
if (!durationStr || typeof durationStr !== 'string') {
|
|
28
|
+
throw new DurationParseError(durationStr, 'Duration must be a non-empty string');
|
|
29
|
+
}
|
|
30
|
+
// Remove all spaces and convert to lowercase
|
|
31
|
+
const normalized = durationStr.trim().toLowerCase().replace(/\s+/g, '');
|
|
32
|
+
// Check for empty string after trimming
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
throw new DurationParseError(durationStr, 'Duration must be a non-empty string');
|
|
35
|
+
}
|
|
36
|
+
// Validate format with strict ordering: hours must come before minutes
|
|
37
|
+
// This regex ensures: optional hours followed by optional minutes, no duplicates
|
|
38
|
+
const match = normalized.match(/^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?$/);
|
|
39
|
+
if (!match || (!match[1] && !match[2])) {
|
|
40
|
+
throw new DurationParseError(durationStr, 'Use format like "2h", "30m", "2h30m", or "1.5h"');
|
|
41
|
+
}
|
|
42
|
+
let totalMinutes = 0;
|
|
43
|
+
const [, hoursStr, minutesStr] = match;
|
|
44
|
+
// Parse hours if present
|
|
45
|
+
if (hoursStr) {
|
|
46
|
+
const hours = Number.parseFloat(hoursStr);
|
|
47
|
+
if (Number.isNaN(hours) || hours < 0) {
|
|
48
|
+
throw new DurationParseError(durationStr, 'Hours must be a positive number');
|
|
49
|
+
}
|
|
50
|
+
totalMinutes += hours * 60;
|
|
51
|
+
}
|
|
52
|
+
// Parse minutes if present
|
|
53
|
+
if (minutesStr) {
|
|
54
|
+
const minutes = Number.parseFloat(minutesStr);
|
|
55
|
+
if (Number.isNaN(minutes) || minutes < 0) {
|
|
56
|
+
throw new DurationParseError(durationStr, 'Minutes must be a positive number');
|
|
57
|
+
}
|
|
58
|
+
// Don't allow decimal minutes
|
|
59
|
+
if (minutes % 1 !== 0) {
|
|
60
|
+
throw new DurationParseError(durationStr, 'Minutes must be a whole number (use decimal hours instead)');
|
|
61
|
+
}
|
|
62
|
+
totalMinutes += minutes;
|
|
63
|
+
}
|
|
64
|
+
// The regex already ensures at least one unit is present
|
|
65
|
+
// Round to nearest minute (handles decimal hours)
|
|
66
|
+
totalMinutes = Math.round(totalMinutes);
|
|
67
|
+
// Validate minimum duration
|
|
68
|
+
if (totalMinutes === 0) {
|
|
69
|
+
throw new DurationParseError(durationStr, 'Duration must be greater than 0 minutes');
|
|
70
|
+
}
|
|
71
|
+
// Validate maximum duration (24 hours = 1440 minutes)
|
|
72
|
+
if (totalMinutes > 1440) {
|
|
73
|
+
throw new DurationParseError(durationStr, 'Duration cannot exceed 24 hours (1440 minutes)');
|
|
74
|
+
}
|
|
75
|
+
return { minutes: totalMinutes };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Formats minutes back to a human-readable duration string.
|
|
79
|
+
* Used when returning task data to LLMs.
|
|
80
|
+
*
|
|
81
|
+
* @param minutes - Duration in minutes
|
|
82
|
+
* @returns Formatted duration string like "2h30m" or "45m"
|
|
83
|
+
*/
|
|
84
|
+
export function formatDuration(minutes) {
|
|
85
|
+
if (minutes <= 0)
|
|
86
|
+
return '0m';
|
|
87
|
+
const hours = Math.floor(minutes / 60);
|
|
88
|
+
const remainingMinutes = minutes % 60;
|
|
89
|
+
if (hours === 0) {
|
|
90
|
+
return `${remainingMinutes}m`;
|
|
91
|
+
}
|
|
92
|
+
if (remainingMinutes === 0) {
|
|
93
|
+
return `${hours}h`;
|
|
94
|
+
}
|
|
95
|
+
return `${hours}h${remainingMinutes}m`;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration-parser.test.d.ts","sourceRoot":"","sources":["../../src/utils/duration-parser.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DurationParseError, formatDuration, parseDuration } from './duration-parser.js';
|
|
2
|
+
describe('parseDuration', () => {
|
|
3
|
+
describe('valid formats', () => {
|
|
4
|
+
it('should parse hours only', () => {
|
|
5
|
+
expect(parseDuration('2h')).toEqual({ minutes: 120 });
|
|
6
|
+
expect(parseDuration('1h')).toEqual({ minutes: 60 });
|
|
7
|
+
expect(parseDuration('24h')).toEqual({ minutes: 1440 });
|
|
8
|
+
});
|
|
9
|
+
it('should parse minutes only', () => {
|
|
10
|
+
expect(parseDuration('90m')).toEqual({ minutes: 90 });
|
|
11
|
+
expect(parseDuration('45m')).toEqual({ minutes: 45 });
|
|
12
|
+
expect(parseDuration('1m')).toEqual({ minutes: 1 });
|
|
13
|
+
expect(parseDuration('1440m')).toEqual({ minutes: 1440 });
|
|
14
|
+
});
|
|
15
|
+
it('should parse hours and minutes combined', () => {
|
|
16
|
+
expect(parseDuration('2h30m')).toEqual({ minutes: 150 });
|
|
17
|
+
expect(parseDuration('1h45m')).toEqual({ minutes: 105 });
|
|
18
|
+
expect(parseDuration('0h30m')).toEqual({ minutes: 30 });
|
|
19
|
+
expect(parseDuration('23h59m')).toEqual({ minutes: 1439 });
|
|
20
|
+
});
|
|
21
|
+
it('should parse decimal hours', () => {
|
|
22
|
+
expect(parseDuration('1.5h')).toEqual({ minutes: 90 });
|
|
23
|
+
expect(parseDuration('2.25h')).toEqual({ minutes: 135 });
|
|
24
|
+
expect(parseDuration('0.5h')).toEqual({ minutes: 30 });
|
|
25
|
+
expect(parseDuration('0.75h')).toEqual({ minutes: 45 });
|
|
26
|
+
});
|
|
27
|
+
it('should handle spaces in input', () => {
|
|
28
|
+
expect(parseDuration('2h 30m')).toEqual({ minutes: 150 });
|
|
29
|
+
expect(parseDuration(' 1h45m ')).toEqual({ minutes: 105 });
|
|
30
|
+
expect(parseDuration(' 2h ')).toEqual({ minutes: 120 });
|
|
31
|
+
expect(parseDuration(' 90m ')).toEqual({ minutes: 90 });
|
|
32
|
+
});
|
|
33
|
+
it('should handle case insensitive input', () => {
|
|
34
|
+
expect(parseDuration('2H')).toEqual({ minutes: 120 });
|
|
35
|
+
expect(parseDuration('90M')).toEqual({ minutes: 90 });
|
|
36
|
+
expect(parseDuration('2H30M')).toEqual({ minutes: 150 });
|
|
37
|
+
expect(parseDuration('1.5H')).toEqual({ minutes: 90 });
|
|
38
|
+
});
|
|
39
|
+
it('should round decimal minutes from decimal hours', () => {
|
|
40
|
+
expect(parseDuration('1.33h')).toEqual({ minutes: 80 }); // 1.33 * 60 = 79.8 -> 80
|
|
41
|
+
expect(parseDuration('1.67h')).toEqual({ minutes: 100 }); // 1.67 * 60 = 100.2 -> 100
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('invalid formats', () => {
|
|
45
|
+
it('should throw error for empty or null input', () => {
|
|
46
|
+
expect(() => parseDuration('')).toThrow(DurationParseError);
|
|
47
|
+
expect(() => parseDuration(' ')).toThrow('Duration must be a non-empty string');
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: Testing error cases with invalid types
|
|
49
|
+
expect(() => parseDuration(null)).toThrow('Duration must be a non-empty string');
|
|
50
|
+
// biome-ignore lint/suspicious/noExplicitAny: Testing error cases with invalid types
|
|
51
|
+
expect(() => parseDuration(undefined)).toThrow('Duration must be a non-empty string');
|
|
52
|
+
});
|
|
53
|
+
it('should throw error for invalid format', () => {
|
|
54
|
+
expect(() => parseDuration('2')).toThrow('Use format like "2h", "30m", "2h30m", or "1.5h"');
|
|
55
|
+
expect(() => parseDuration('2hours')).toThrow('Use format like');
|
|
56
|
+
expect(() => parseDuration('2h30')).toThrow('Use format like');
|
|
57
|
+
expect(() => parseDuration('h30m')).toThrow('Use format like');
|
|
58
|
+
expect(() => parseDuration('2x30m')).toThrow('Use format like');
|
|
59
|
+
expect(() => parseDuration('2h30s')).toThrow('Use format like');
|
|
60
|
+
});
|
|
61
|
+
it('should throw error for decimal minutes', () => {
|
|
62
|
+
expect(() => parseDuration('90.5m')).toThrow('Minutes must be a whole number');
|
|
63
|
+
expect(() => parseDuration('1h30.5m')).toThrow('Minutes must be a whole number');
|
|
64
|
+
});
|
|
65
|
+
it('should throw error for negative values', () => {
|
|
66
|
+
expect(() => parseDuration('-2h')).toThrow('Use format like');
|
|
67
|
+
expect(() => parseDuration('-30m')).toThrow('Use format like');
|
|
68
|
+
expect(() => parseDuration('2h-30m')).toThrow('Use format like');
|
|
69
|
+
});
|
|
70
|
+
it('should throw error for zero duration', () => {
|
|
71
|
+
expect(() => parseDuration('0h')).toThrow('Duration must be greater than 0 minutes');
|
|
72
|
+
expect(() => parseDuration('0m')).toThrow('Duration must be greater than 0 minutes');
|
|
73
|
+
expect(() => parseDuration('0h0m')).toThrow('Duration must be greater than 0 minutes');
|
|
74
|
+
});
|
|
75
|
+
it('should throw error for duration exceeding 24 hours', () => {
|
|
76
|
+
expect(() => parseDuration('25h')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
77
|
+
expect(() => parseDuration('1441m')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
78
|
+
expect(() => parseDuration('24h1m')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
79
|
+
expect(() => parseDuration('24.1h')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
|
|
80
|
+
});
|
|
81
|
+
it('should throw error for malformed numbers', () => {
|
|
82
|
+
expect(() => parseDuration('2.h')).toThrow('Use format like');
|
|
83
|
+
expect(() => parseDuration('2h.m')).toThrow('Use format like');
|
|
84
|
+
expect(() => parseDuration('2..5h')).toThrow('Use format like');
|
|
85
|
+
});
|
|
86
|
+
it('should throw error for duplicate units', () => {
|
|
87
|
+
expect(() => parseDuration('2h3h')).toThrow('Use format like');
|
|
88
|
+
expect(() => parseDuration('30m45m')).toThrow('Use format like');
|
|
89
|
+
});
|
|
90
|
+
it('should throw error for wrong order (minutes before hours)', () => {
|
|
91
|
+
expect(() => parseDuration('30m2h')).toThrow('Use format like');
|
|
92
|
+
expect(() => parseDuration('45m1h')).toThrow('Use format like');
|
|
93
|
+
});
|
|
94
|
+
it('should throw error for invalid mixed formats with correct order', () => {
|
|
95
|
+
expect(() => parseDuration('2h30m15h')).toThrow('Use format like');
|
|
96
|
+
expect(() => parseDuration('1h2h30m')).toThrow('Use format like');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('edge cases', () => {
|
|
100
|
+
it('should handle maximum allowed duration', () => {
|
|
101
|
+
expect(parseDuration('24h')).toEqual({ minutes: 1440 });
|
|
102
|
+
expect(parseDuration('1440m')).toEqual({ minutes: 1440 });
|
|
103
|
+
expect(parseDuration('23h60m')).toEqual({ minutes: 1440 });
|
|
104
|
+
});
|
|
105
|
+
it('should handle minimum allowed duration', () => {
|
|
106
|
+
expect(parseDuration('1m')).toEqual({ minutes: 1 });
|
|
107
|
+
expect(parseDuration('0.017h')).toEqual({ minutes: 1 }); // 0.017 * 60 = 1.02 -> 1
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('formatDuration', () => {
|
|
112
|
+
it('should format minutes only', () => {
|
|
113
|
+
expect(formatDuration(45)).toBe('45m');
|
|
114
|
+
expect(formatDuration(1)).toBe('1m');
|
|
115
|
+
expect(formatDuration(59)).toBe('59m');
|
|
116
|
+
});
|
|
117
|
+
it('should format hours only', () => {
|
|
118
|
+
expect(formatDuration(60)).toBe('1h');
|
|
119
|
+
expect(formatDuration(120)).toBe('2h');
|
|
120
|
+
expect(formatDuration(1440)).toBe('24h');
|
|
121
|
+
});
|
|
122
|
+
it('should format hours and minutes combined', () => {
|
|
123
|
+
expect(formatDuration(90)).toBe('1h30m');
|
|
124
|
+
expect(formatDuration(150)).toBe('2h30m');
|
|
125
|
+
expect(formatDuration(105)).toBe('1h45m');
|
|
126
|
+
expect(formatDuration(1439)).toBe('23h59m');
|
|
127
|
+
});
|
|
128
|
+
it('should handle edge cases', () => {
|
|
129
|
+
expect(formatDuration(0)).toBe('0m');
|
|
130
|
+
expect(formatDuration(-5)).toBe('0m');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('round trip parsing and formatting', () => {
|
|
134
|
+
const testCases = [
|
|
135
|
+
{ input: '2h', expectedMinutes: 120, expectedFormat: '2h' },
|
|
136
|
+
{ input: '45m', expectedMinutes: 45, expectedFormat: '45m' },
|
|
137
|
+
{ input: '2h30m', expectedMinutes: 150, expectedFormat: '2h30m' },
|
|
138
|
+
{ input: '1.5h', expectedMinutes: 90, expectedFormat: '1h30m' },
|
|
139
|
+
];
|
|
140
|
+
for (const { input, expectedMinutes, expectedFormat } of testCases) {
|
|
141
|
+
it(`should parse "${input}" and format back consistently`, () => {
|
|
142
|
+
const parsed = parseDuration(input);
|
|
143
|
+
expect(parsed.minutes).toBe(expectedMinutes);
|
|
144
|
+
expect(formatDuration(parsed.minutes)).toBe(expectedFormat);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/todoist-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"main": "./dist/index.js",
|
|
5
6
|
"types": "./dist/index.d.ts",
|
|
6
7
|
"files": [
|
|
7
8
|
"dist",
|
|
9
|
+
"scripts",
|
|
8
10
|
"package.json",
|
|
9
11
|
"LICENSE.txt",
|
|
10
12
|
"README.md"
|
|
@@ -23,8 +25,10 @@
|
|
|
23
25
|
"scripts": {
|
|
24
26
|
"test": "jest",
|
|
25
27
|
"build": "rimraf dist && npx tsc --project tsconfig.json",
|
|
26
|
-
"
|
|
28
|
+
"start": "npm run build && npx @modelcontextprotocol/inspector node dist/main.js",
|
|
29
|
+
"dev": "concurrently \"npx tsc --watch\" \"nodemon --watch dist --ext js --exec 'npx @modelcontextprotocol/inspector node dist/main.js'\"",
|
|
27
30
|
"setup": "cp .env.example .env && npm install && npm run build",
|
|
31
|
+
"test:executable": "npm run build && node scripts/test-executable.cjs",
|
|
28
32
|
"type-check": "npx tsc --noEmit",
|
|
29
33
|
"biome:sort-imports": "biome check --formatter-enabled=false --linter-enabled=false --organize-imports-enabled=true --write .",
|
|
30
34
|
"lint:check": "biome lint",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
|
|
6
|
+
console.log('Testing MCP server executable...')
|
|
7
|
+
|
|
8
|
+
const mainJs = path.join(__dirname, '..', 'dist', 'main.js')
|
|
9
|
+
const child = spawn('node', [mainJs], {
|
|
10
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
let _stdoutOutput = ''
|
|
14
|
+
let stderrOutput = ''
|
|
15
|
+
let hasError = false
|
|
16
|
+
|
|
17
|
+
child.stdout.on('data', (data) => {
|
|
18
|
+
_stdoutOutput += data.toString()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
child.stderr.on('data', (data) => {
|
|
22
|
+
const output = data.toString()
|
|
23
|
+
stderrOutput += output
|
|
24
|
+
|
|
25
|
+
// Only consider it an error if it's not related to graceful shutdown
|
|
26
|
+
if (output.includes('Error:') && !output.includes('SIGTERM') && !output.includes('SIGKILL')) {
|
|
27
|
+
console.error('Server startup error detected:', output)
|
|
28
|
+
hasError = true
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
child.on('error', (error) => {
|
|
33
|
+
console.error('Failed to start MCP server:', error.message)
|
|
34
|
+
hasError = true
|
|
35
|
+
process.exit(1)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
child.on('exit', (code, signal) => {
|
|
39
|
+
// Expected signals when we kill the process
|
|
40
|
+
if (signal === 'SIGTERM' || signal === 'SIGKILL') {
|
|
41
|
+
return // This is expected
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Unexpected exit codes during startup
|
|
45
|
+
if (code !== null && code !== 0) {
|
|
46
|
+
console.error(`Server exited unexpectedly with code ${code}`)
|
|
47
|
+
hasError = true
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Kill the process after 2 seconds (MCP server should start successfully)
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
if (hasError) {
|
|
54
|
+
console.error('❌ MCP server failed to start properly')
|
|
55
|
+
if (stderrOutput.trim()) {
|
|
56
|
+
console.error('Error output:', stderrOutput.trim())
|
|
57
|
+
}
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Gracefully terminate
|
|
62
|
+
child.kill('SIGTERM')
|
|
63
|
+
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
console.log('✅ MCP server executable test passed')
|
|
66
|
+
console.log('Server started successfully and is ready to accept connections')
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}, 200) // Give it a moment to clean up
|
|
69
|
+
}, 2000)
|