@doist/todoist-ai 4.9.4 → 4.10.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 +11 -0
- package/dist/index.d.ts +37 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/mcp-helpers.d.ts +8 -1
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +5 -0
- package/dist/tool-helpers.d.ts +8 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +13 -10
- package/dist/tools/__tests__/fetch.test.d.ts +2 -0
- package/dist/tools/__tests__/fetch.test.d.ts.map +1 -0
- package/dist/tools/__tests__/fetch.test.js +275 -0
- package/dist/tools/__tests__/search.test.d.ts +2 -0
- package/dist/tools/__tests__/search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/search.test.js +208 -0
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/add-tasks.js +8 -2
- package/dist/tools/fetch.d.ts +26 -0
- package/dist/tools/fetch.d.ts.map +1 -0
- package/dist/tools/fetch.js +99 -0
- package/dist/tools/find-projects.d.ts +2 -2
- package/dist/tools/search.d.ts +26 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +64 -0
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +8 -2
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/tool-names.d.ts +2 -0
- package/dist/utils/tool-names.d.ts.map +1 -1
- package/dist/utils/tool-names.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,10 +74,17 @@ Then enable the server in Cursor settings if prompted.
|
|
|
74
74
|
|
|
75
75
|
#### Claude Code (CLI)
|
|
76
76
|
|
|
77
|
+
Firstly configure Claude so it has a new MCP available using this command:
|
|
78
|
+
|
|
77
79
|
```bash
|
|
78
80
|
claude mcp add --transport http todoist https://ai.todoist.net/mcp
|
|
79
81
|
```
|
|
80
82
|
|
|
83
|
+
Then launch `claude`, execute `/mcp`, then select the `todoist` MCP server.
|
|
84
|
+
|
|
85
|
+
This will take you through a wizard to authenticate using your browser with Todoist. Once complete you will be able to use todoist in `claude`.
|
|
86
|
+
|
|
87
|
+
|
|
81
88
|
#### Visual Studio Code
|
|
82
89
|
|
|
83
90
|
1. Open Command Palette → MCP: Add Server
|
|
@@ -116,6 +123,10 @@ For our design philosophy, guidelines, and development patterns, see [docs/tool-
|
|
|
116
123
|
|
|
117
124
|
For a complete list of available tools, see the [src/tools](src/tools) directory.
|
|
118
125
|
|
|
126
|
+
#### OpenAI MCP Compatibility
|
|
127
|
+
|
|
128
|
+
This server includes `search` and `fetch` tools that follow the [OpenAI MCP specification](https://platform.openai.com/docs/mcp), enabling seamless integration with OpenAI's MCP protocol. These tools return JSON-encoded results optimized for OpenAI's requirements while maintaining compatibility with the broader MCP ecosystem.
|
|
129
|
+
|
|
119
130
|
## Dependencies
|
|
120
131
|
|
|
121
132
|
- MCP server using the official [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#installation)
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { addSections } from './tools/add-sections.js';
|
|
|
5
5
|
import { addTasks } from './tools/add-tasks.js';
|
|
6
6
|
import { completeTasks } from './tools/complete-tasks.js';
|
|
7
7
|
import { deleteObject } from './tools/delete-object.js';
|
|
8
|
+
import { fetch } from './tools/fetch.js';
|
|
8
9
|
import { findComments } from './tools/find-comments.js';
|
|
9
10
|
import { findCompletedTasks } from './tools/find-completed-tasks.js';
|
|
10
11
|
import { findProjectCollaborators } from './tools/find-project-collaborators.js';
|
|
@@ -14,6 +15,7 @@ import { findTasks } from './tools/find-tasks.js';
|
|
|
14
15
|
import { findTasksByDate } from './tools/find-tasks-by-date.js';
|
|
15
16
|
import { getOverview } from './tools/get-overview.js';
|
|
16
17
|
import { manageAssignments } from './tools/manage-assignments.js';
|
|
18
|
+
import { search } from './tools/search.js';
|
|
17
19
|
import { updateComments } from './tools/update-comments.js';
|
|
18
20
|
import { updateProjects } from './tools/update-projects.js';
|
|
19
21
|
import { updateSections } from './tools/update-sections.js';
|
|
@@ -673,8 +675,8 @@ declare const tools: {
|
|
|
673
675
|
};
|
|
674
676
|
execute(args: {
|
|
675
677
|
limit: number;
|
|
676
|
-
cursor?: string | undefined;
|
|
677
678
|
search?: string | undefined;
|
|
679
|
+
cursor?: string | undefined;
|
|
678
680
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
679
681
|
content: {
|
|
680
682
|
type: "text";
|
|
@@ -696,8 +698,8 @@ declare const tools: {
|
|
|
696
698
|
hasMore: boolean;
|
|
697
699
|
appliedFilters: {
|
|
698
700
|
limit: number;
|
|
699
|
-
cursor?: string | undefined;
|
|
700
701
|
search?: string | undefined;
|
|
702
|
+
cursor?: string | undefined;
|
|
701
703
|
};
|
|
702
704
|
};
|
|
703
705
|
} | {
|
|
@@ -1257,7 +1259,39 @@ declare const tools: {
|
|
|
1257
1259
|
structuredContent?: undefined;
|
|
1258
1260
|
}>;
|
|
1259
1261
|
};
|
|
1262
|
+
search: {
|
|
1263
|
+
name: "search";
|
|
1264
|
+
description: string;
|
|
1265
|
+
parameters: {
|
|
1266
|
+
query: import("zod").ZodString;
|
|
1267
|
+
};
|
|
1268
|
+
execute(args: {
|
|
1269
|
+
query: string;
|
|
1270
|
+
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
1271
|
+
content: {
|
|
1272
|
+
type: "text";
|
|
1273
|
+
text: string;
|
|
1274
|
+
}[];
|
|
1275
|
+
isError?: boolean;
|
|
1276
|
+
}>;
|
|
1277
|
+
};
|
|
1278
|
+
fetch: {
|
|
1279
|
+
name: "fetch";
|
|
1280
|
+
description: string;
|
|
1281
|
+
parameters: {
|
|
1282
|
+
id: import("zod").ZodString;
|
|
1283
|
+
};
|
|
1284
|
+
execute(args: {
|
|
1285
|
+
id: string;
|
|
1286
|
+
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
1287
|
+
content: {
|
|
1288
|
+
type: "text";
|
|
1289
|
+
text: string;
|
|
1290
|
+
}[];
|
|
1291
|
+
isError?: boolean;
|
|
1292
|
+
}>;
|
|
1293
|
+
};
|
|
1260
1294
|
};
|
|
1261
1295
|
export { tools, getMcpServer };
|
|
1262
|
-
export { addTasks, completeTasks, updateTasks, findTasks, findTasksByDate, findCompletedTasks, addProjects, updateProjects, findProjects, addSections, updateSections, findSections, addComments, updateComments, findComments, getOverview, deleteObject, userInfo, findProjectCollaborators, manageAssignments, };
|
|
1296
|
+
export { addTasks, completeTasks, updateTasks, findTasks, findTasksByDate, findCompletedTasks, addProjects, updateProjects, findProjects, addSections, updateSections, findSections, addComments, updateComments, findComments, getOverview, deleteObject, userInfo, findProjectCollaborators, manageAssignments, search, fetch, };
|
|
1263
1297
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AAEpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAA;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,QAAA,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { addTasks } from './tools/add-tasks.js';
|
|
|
10
10
|
import { completeTasks } from './tools/complete-tasks.js';
|
|
11
11
|
// General tools
|
|
12
12
|
import { deleteObject } from './tools/delete-object.js';
|
|
13
|
+
import { fetch } from './tools/fetch.js';
|
|
13
14
|
import { findComments } from './tools/find-comments.js';
|
|
14
15
|
import { findCompletedTasks } from './tools/find-completed-tasks.js';
|
|
15
16
|
// Assignment and collaboration tools
|
|
@@ -20,6 +21,7 @@ import { findTasks } from './tools/find-tasks.js';
|
|
|
20
21
|
import { findTasksByDate } from './tools/find-tasks-by-date.js';
|
|
21
22
|
import { getOverview } from './tools/get-overview.js';
|
|
22
23
|
import { manageAssignments } from './tools/manage-assignments.js';
|
|
24
|
+
import { search } from './tools/search.js';
|
|
23
25
|
import { updateComments } from './tools/update-comments.js';
|
|
24
26
|
import { updateProjects } from './tools/update-projects.js';
|
|
25
27
|
import { updateSections } from './tools/update-sections.js';
|
|
@@ -52,6 +54,9 @@ const tools = {
|
|
|
52
54
|
// Assignment and collaboration tools
|
|
53
55
|
findProjectCollaborators,
|
|
54
56
|
manageAssignments,
|
|
57
|
+
// OpenAI MCP tools
|
|
58
|
+
search,
|
|
59
|
+
fetch,
|
|
55
60
|
};
|
|
56
61
|
export { tools, getMcpServer };
|
|
57
62
|
export {
|
|
@@ -66,4 +71,6 @@ addComments, updateComments, findComments,
|
|
|
66
71
|
// General tools
|
|
67
72
|
getOverview, deleteObject, userInfo,
|
|
68
73
|
// Assignment and collaboration tools
|
|
69
|
-
findProjectCollaborators, manageAssignments,
|
|
74
|
+
findProjectCollaborators, manageAssignments,
|
|
75
|
+
// OpenAI MCP tools
|
|
76
|
+
search, fetch, };
|
package/dist/mcp-helpers.d.ts
CHANGED
|
@@ -31,6 +31,13 @@ declare function getToolOutput<StructuredContent extends Record<string, unknown>
|
|
|
31
31
|
})[];
|
|
32
32
|
structuredContent?: undefined;
|
|
33
33
|
};
|
|
34
|
+
declare function getErrorOutput(error: string): {
|
|
35
|
+
content: {
|
|
36
|
+
type: "text";
|
|
37
|
+
text: string;
|
|
38
|
+
}[];
|
|
39
|
+
isError: boolean;
|
|
40
|
+
};
|
|
34
41
|
/**
|
|
35
42
|
* Register a Todoist tool in an MCP server.
|
|
36
43
|
* @param tool - The tool to register.
|
|
@@ -38,5 +45,5 @@ declare function getToolOutput<StructuredContent extends Record<string, unknown>
|
|
|
38
45
|
* @param client - The Todoist API client to use to execute the tool.
|
|
39
46
|
*/
|
|
40
47
|
declare function registerTool<Params extends z.ZodRawShape>(tool: TodoistTool<Params>, server: McpServer, client: TodoistApi): void;
|
|
41
|
-
export { registerTool, getToolOutput };
|
|
48
|
+
export { registerTool, getErrorOutput, getToolOutput };
|
|
42
49
|
//# sourceMappingURL=mcp-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAkBA;
|
|
1
|
+
{"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAkBA;AAED,iBAAS,cAAc,CAAC,KAAK,EAAE,MAAM;;;;;;EAKpC;AAED;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAC9C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,QAqBrB;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA"}
|
package/dist/mcp-helpers.js
CHANGED
package/dist/mcp-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AA0FnE;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,aAkD5F;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
|
package/dist/mcp-server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { addSections } from './tools/add-sections.js';
|
|
|
7
7
|
import { addTasks } from './tools/add-tasks.js';
|
|
8
8
|
import { completeTasks } from './tools/complete-tasks.js';
|
|
9
9
|
import { deleteObject } from './tools/delete-object.js';
|
|
10
|
+
import { fetch } from './tools/fetch.js';
|
|
10
11
|
import { findComments } from './tools/find-comments.js';
|
|
11
12
|
import { findCompletedTasks } from './tools/find-completed-tasks.js';
|
|
12
13
|
import { findProjectCollaborators } from './tools/find-project-collaborators.js';
|
|
@@ -16,6 +17,7 @@ import { findTasks } from './tools/find-tasks.js';
|
|
|
16
17
|
import { findTasksByDate } from './tools/find-tasks-by-date.js';
|
|
17
18
|
import { getOverview } from './tools/get-overview.js';
|
|
18
19
|
import { manageAssignments } from './tools/manage-assignments.js';
|
|
20
|
+
import { search } from './tools/search.js';
|
|
19
21
|
import { updateComments } from './tools/update-comments.js';
|
|
20
22
|
import { updateProjects } from './tools/update-projects.js';
|
|
21
23
|
import { updateSections } from './tools/update-sections.js';
|
|
@@ -125,6 +127,9 @@ function getMcpServer({ todoistApiKey, baseUrl }) {
|
|
|
125
127
|
// Assignment and collaboration tools
|
|
126
128
|
registerTool(findProjectCollaborators, server, todoist);
|
|
127
129
|
registerTool(manageAssignments, server, todoist);
|
|
130
|
+
// OpenAI MCP tools
|
|
131
|
+
registerTool(search, server, todoist);
|
|
132
|
+
registerTool(fetch, server, todoist);
|
|
128
133
|
return server;
|
|
129
134
|
}
|
|
130
135
|
export { getMcpServer };
|
package/dist/tool-helpers.d.ts
CHANGED
|
@@ -89,5 +89,12 @@ declare function getTasksByFilter({ client, query, limit, cursor, }: {
|
|
|
89
89
|
}[];
|
|
90
90
|
nextCursor: string | null;
|
|
91
91
|
}>;
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Build a Todoist URL for a task or project.
|
|
94
|
+
* @param type - The type of object ('task' or 'project')
|
|
95
|
+
* @param id - The ID of the object
|
|
96
|
+
* @returns The URL string
|
|
97
|
+
*/
|
|
98
|
+
declare function buildTodoistUrl(type: 'task' | 'project', id: string): string;
|
|
99
|
+
export { getTasksByFilter, mapTask, mapProject, buildTodoistUrl };
|
|
93
100
|
//# sourceMappingURL=tool-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-helpers.d.ts","sourceRoot":"","sources":["../src/tool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,UAAU,EACV,gBAAgB,EACnB,MAAM,+BAA+B,CAAA;AAItC,eAAO,MAAM,0BAA0B,gDAAiD,CAAA;AACxF,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAA;AAElF,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,CAAA;AAExD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,CAE9E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAEhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,EACtF,KAAK,EACL,kBAAkB,EAClB,aAAa,EACb,wBAA2C,GAC9C,EAAE;IACC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,aAAa,EAAE,MAAM,CAAA;IACrB,wBAAwB,CAAC,EAAE,wBAAwB,CAAA;CACtD,GAAG,CAAC,EAAE,CAUN;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAClB,YAAY,CAsBd;AAED;;;;GAIG;AACH,iBAAS,OAAO,CAAC,IAAI,EAAE,IAAI;;;;;;;;;;;;;;EAgB1B;AAED;;;;GAIG;AACH,iBAAS,UAAU,CAAC,OAAO,EAAE,OAAO;;;;;;;;;EAWnC;AAWD,iBAAe,gBAAgB,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,KAAK,EACL,MAAM,GACT,EAAE;IACC,MAAM,EAAE,UAAU,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"tool-helpers.d.ts","sourceRoot":"","sources":["../src/tool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,UAAU,EACV,gBAAgB,EACnB,MAAM,+BAA+B,CAAA;AAItC,eAAO,MAAM,0BAA0B,gDAAiD,CAAA;AACxF,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAA;AAElF,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,CAAA;AAExD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,CAE9E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAEhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,EACtF,KAAK,EACL,kBAAkB,EAClB,aAAa,EACb,wBAA2C,GAC9C,EAAE;IACC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,aAAa,EAAE,MAAM,CAAA;IACrB,wBAAwB,CAAC,EAAE,wBAAwB,CAAA;CACtD,GAAG,CAAC,EAAE,CAUN;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAClB,YAAY,CAsBd;AAED;;;;GAIG;AACH,iBAAS,OAAO,CAAC,IAAI,EAAE,IAAI;;;;;;;;;;;;;;EAgB1B;AAED;;;;GAIG;AACH,iBAAS,UAAU,CAAC,OAAO,EAAE,OAAO;;;;;;;;;EAWnC;AAWD,iBAAe,gBAAgB,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,KAAK,EACL,MAAM,GACT,EAAE;IACC,MAAM,EAAE,UAAU,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;;;;;;;;;;;;;;;;;GAkBA;AAED;;;;;GAKG;AACH,iBAAS,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,CAAA"}
|
package/dist/tool-helpers.js
CHANGED
|
@@ -105,15 +105,9 @@ const ErrorSchema = z.object({
|
|
|
105
105
|
});
|
|
106
106
|
async function getTasksByFilter({ client, query, limit, cursor, }) {
|
|
107
107
|
try {
|
|
108
|
-
const { results, nextCursor } = await client.getTasksByFilter({
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
limit,
|
|
112
|
-
});
|
|
113
|
-
return {
|
|
114
|
-
tasks: results.map(mapTask),
|
|
115
|
-
nextCursor,
|
|
116
|
-
};
|
|
108
|
+
const { results, nextCursor } = await client.getTasksByFilter({ query, cursor, limit });
|
|
109
|
+
const tasks = results.map(mapTask);
|
|
110
|
+
return { tasks, nextCursor };
|
|
117
111
|
}
|
|
118
112
|
catch (error) {
|
|
119
113
|
const parsedError = ErrorSchema.safeParse(error);
|
|
@@ -127,4 +121,13 @@ async function getTasksByFilter({ client, query, limit, cursor, }) {
|
|
|
127
121
|
throw new Error(`${responseData.error} (tag: ${responseData.errorTag}, code: ${responseData.errorCode})`);
|
|
128
122
|
}
|
|
129
123
|
}
|
|
130
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Build a Todoist URL for a task or project.
|
|
126
|
+
* @param type - The type of object ('task' or 'project')
|
|
127
|
+
* @param id - The ID of the object
|
|
128
|
+
* @returns The URL string
|
|
129
|
+
*/
|
|
130
|
+
function buildTodoistUrl(type, id) {
|
|
131
|
+
return `https://app.todoist.com/app/${type}/${id}`;
|
|
132
|
+
}
|
|
133
|
+
export { getTasksByFilter, mapTask, mapProject, buildTodoistUrl };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/fetch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { createMockProject, createMockTask, TEST_IDS } from '../../utils/test-helpers.js';
|
|
3
|
+
import { ToolNames } from '../../utils/tool-names.js';
|
|
4
|
+
import { fetch } from '../fetch.js';
|
|
5
|
+
// Mock the Todoist API
|
|
6
|
+
const mockTodoistApi = {
|
|
7
|
+
getTask: jest.fn(),
|
|
8
|
+
getProject: jest.fn(),
|
|
9
|
+
};
|
|
10
|
+
const { FETCH } = ToolNames;
|
|
11
|
+
describe(`${FETCH} tool`, () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
describe('fetching tasks', () => {
|
|
16
|
+
it('should fetch a task by composite ID and return full content', async () => {
|
|
17
|
+
const mockTask = createMockTask({
|
|
18
|
+
id: TEST_IDS.TASK_1,
|
|
19
|
+
content: 'Important meeting with team',
|
|
20
|
+
description: 'Discuss project roadmap and timeline',
|
|
21
|
+
labels: ['work', 'urgent'],
|
|
22
|
+
priority: 2,
|
|
23
|
+
projectId: TEST_IDS.PROJECT_WORK,
|
|
24
|
+
sectionId: TEST_IDS.SECTION_1,
|
|
25
|
+
due: {
|
|
26
|
+
date: '2025-10-15',
|
|
27
|
+
isRecurring: false,
|
|
28
|
+
datetime: null,
|
|
29
|
+
string: '2025-10-15',
|
|
30
|
+
timezone: null,
|
|
31
|
+
lang: 'en',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
35
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
36
|
+
// Verify API was called correctly
|
|
37
|
+
expect(mockTodoistApi.getTask).toHaveBeenCalledWith(TEST_IDS.TASK_1);
|
|
38
|
+
// Verify result structure
|
|
39
|
+
expect(result.content).toHaveLength(1);
|
|
40
|
+
expect(result.content[0]?.type).toBe('text');
|
|
41
|
+
// Parse the JSON response
|
|
42
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
43
|
+
expect(jsonResponse).toEqual({
|
|
44
|
+
id: `task:${TEST_IDS.TASK_1}`,
|
|
45
|
+
title: 'Important meeting with team',
|
|
46
|
+
text: 'Important meeting with team\n\nDescription: Discuss project roadmap and timeline\nDue: 2025-10-15\nLabels: work, urgent',
|
|
47
|
+
url: `https://app.todoist.com/app/task/${TEST_IDS.TASK_1}`,
|
|
48
|
+
metadata: {
|
|
49
|
+
priority: 2,
|
|
50
|
+
projectId: TEST_IDS.PROJECT_WORK,
|
|
51
|
+
sectionId: TEST_IDS.SECTION_1,
|
|
52
|
+
parentId: null,
|
|
53
|
+
recurring: false,
|
|
54
|
+
duration: null,
|
|
55
|
+
responsibleUid: null,
|
|
56
|
+
assignedByUid: null,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('should fetch a task without optional fields', async () => {
|
|
61
|
+
const mockTask = createMockTask({
|
|
62
|
+
id: TEST_IDS.TASK_2,
|
|
63
|
+
content: 'Simple task',
|
|
64
|
+
description: '',
|
|
65
|
+
labels: [],
|
|
66
|
+
due: null,
|
|
67
|
+
});
|
|
68
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
69
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_2}` }, mockTodoistApi);
|
|
70
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
71
|
+
expect(jsonResponse.title).toBe('Simple task');
|
|
72
|
+
expect(jsonResponse.text).toBe('Simple task');
|
|
73
|
+
expect(jsonResponse.metadata).toEqual({
|
|
74
|
+
priority: 1,
|
|
75
|
+
projectId: TEST_IDS.PROJECT_TEST,
|
|
76
|
+
sectionId: null,
|
|
77
|
+
parentId: null,
|
|
78
|
+
recurring: false,
|
|
79
|
+
duration: null,
|
|
80
|
+
responsibleUid: null,
|
|
81
|
+
assignedByUid: null,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('should handle tasks with recurring due dates', async () => {
|
|
85
|
+
const mockTask = createMockTask({
|
|
86
|
+
id: TEST_IDS.TASK_3,
|
|
87
|
+
content: 'Weekly meeting',
|
|
88
|
+
due: {
|
|
89
|
+
date: '2025-10-15',
|
|
90
|
+
isRecurring: true,
|
|
91
|
+
datetime: null,
|
|
92
|
+
string: 'every monday',
|
|
93
|
+
timezone: null,
|
|
94
|
+
lang: 'en',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
98
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_3}` }, mockTodoistApi);
|
|
99
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
100
|
+
expect(jsonResponse.metadata.recurring).toBe('every monday');
|
|
101
|
+
});
|
|
102
|
+
it('should handle tasks with duration', async () => {
|
|
103
|
+
const mockTask = createMockTask({
|
|
104
|
+
id: TEST_IDS.TASK_1,
|
|
105
|
+
content: 'Task with duration',
|
|
106
|
+
duration: {
|
|
107
|
+
amount: 90,
|
|
108
|
+
unit: 'minute',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
112
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
113
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
114
|
+
expect(jsonResponse.metadata.duration).toBe('1h30m');
|
|
115
|
+
});
|
|
116
|
+
it('should handle tasks with assignments', async () => {
|
|
117
|
+
const mockTask = createMockTask({
|
|
118
|
+
id: TEST_IDS.TASK_1,
|
|
119
|
+
content: 'Assigned task',
|
|
120
|
+
responsibleUid: 'user-123',
|
|
121
|
+
assignedByUid: 'user-456',
|
|
122
|
+
});
|
|
123
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
124
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
125
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
126
|
+
expect(jsonResponse.metadata.responsibleUid).toBe('user-123');
|
|
127
|
+
expect(jsonResponse.metadata.assignedByUid).toBe('user-456');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('fetching projects', () => {
|
|
131
|
+
it('should fetch a project by composite ID and return full content', async () => {
|
|
132
|
+
const mockProject = createMockProject({
|
|
133
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
134
|
+
name: 'Work Project',
|
|
135
|
+
color: 'blue',
|
|
136
|
+
isFavorite: true,
|
|
137
|
+
isShared: true,
|
|
138
|
+
viewStyle: 'board',
|
|
139
|
+
parentId: null,
|
|
140
|
+
inboxProject: false,
|
|
141
|
+
});
|
|
142
|
+
mockTodoistApi.getProject.mockResolvedValue(mockProject);
|
|
143
|
+
const result = await fetch.execute({ id: `project:${TEST_IDS.PROJECT_WORK}` }, mockTodoistApi);
|
|
144
|
+
// Verify API was called correctly
|
|
145
|
+
expect(mockTodoistApi.getProject).toHaveBeenCalledWith(TEST_IDS.PROJECT_WORK);
|
|
146
|
+
// Verify result structure
|
|
147
|
+
expect(result.content).toHaveLength(1);
|
|
148
|
+
expect(result.content[0]?.type).toBe('text');
|
|
149
|
+
// Parse the JSON response
|
|
150
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
151
|
+
expect(jsonResponse).toEqual({
|
|
152
|
+
id: `project:${TEST_IDS.PROJECT_WORK}`,
|
|
153
|
+
title: 'Work Project',
|
|
154
|
+
text: 'Work Project\n\nShared project\nFavorite: Yes',
|
|
155
|
+
url: `https://app.todoist.com/app/project/${TEST_IDS.PROJECT_WORK}`,
|
|
156
|
+
metadata: {
|
|
157
|
+
color: 'blue',
|
|
158
|
+
isFavorite: true,
|
|
159
|
+
isShared: true,
|
|
160
|
+
parentId: null,
|
|
161
|
+
inboxProject: false,
|
|
162
|
+
viewStyle: 'board',
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it('should fetch a project without optional flags', async () => {
|
|
167
|
+
const mockProject = createMockProject({
|
|
168
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
169
|
+
name: 'Simple Project',
|
|
170
|
+
isFavorite: false,
|
|
171
|
+
isShared: false,
|
|
172
|
+
});
|
|
173
|
+
mockTodoistApi.getProject.mockResolvedValue(mockProject);
|
|
174
|
+
const result = await fetch.execute({ id: `project:${TEST_IDS.PROJECT_TEST}` }, mockTodoistApi);
|
|
175
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
176
|
+
expect(jsonResponse.title).toBe('Simple Project');
|
|
177
|
+
expect(jsonResponse.text).toBe('Simple Project');
|
|
178
|
+
expect(jsonResponse.metadata.isFavorite).toBe(false);
|
|
179
|
+
expect(jsonResponse.metadata.isShared).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
it('should fetch inbox project', async () => {
|
|
182
|
+
const mockProject = createMockProject({
|
|
183
|
+
id: TEST_IDS.PROJECT_INBOX,
|
|
184
|
+
name: 'Inbox',
|
|
185
|
+
inboxProject: true,
|
|
186
|
+
});
|
|
187
|
+
mockTodoistApi.getProject.mockResolvedValue(mockProject);
|
|
188
|
+
const result = await fetch.execute({ id: `project:${TEST_IDS.PROJECT_INBOX}` }, mockTodoistApi);
|
|
189
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
190
|
+
expect(jsonResponse.metadata.inboxProject).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
it('should fetch project with parent ID', async () => {
|
|
193
|
+
const mockProject = createMockProject({
|
|
194
|
+
id: 'sub-project-id',
|
|
195
|
+
name: 'Sub Project',
|
|
196
|
+
parentId: TEST_IDS.PROJECT_WORK,
|
|
197
|
+
});
|
|
198
|
+
mockTodoistApi.getProject.mockResolvedValue(mockProject);
|
|
199
|
+
const result = await fetch.execute({ id: 'project:sub-project-id' }, mockTodoistApi);
|
|
200
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
201
|
+
expect(jsonResponse.metadata.parentId).toBe(TEST_IDS.PROJECT_WORK);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('error handling', () => {
|
|
205
|
+
it('should return error response for invalid ID format (missing colon)', async () => {
|
|
206
|
+
const result = await fetch.execute({ id: 'invalid-id' }, mockTodoistApi);
|
|
207
|
+
expect(result.isError).toBe(true);
|
|
208
|
+
expect(result.content[0]?.text).toContain('Invalid ID format');
|
|
209
|
+
});
|
|
210
|
+
it('should return error response for invalid ID format (missing type)', async () => {
|
|
211
|
+
const result = await fetch.execute({ id: ':8485093748' }, mockTodoistApi);
|
|
212
|
+
expect(result.isError).toBe(true);
|
|
213
|
+
expect(result.content[0]?.text).toContain('Invalid ID format');
|
|
214
|
+
});
|
|
215
|
+
it('should return error response for invalid ID format (missing object ID)', async () => {
|
|
216
|
+
const result = await fetch.execute({ id: 'task:' }, mockTodoistApi);
|
|
217
|
+
expect(result.isError).toBe(true);
|
|
218
|
+
expect(result.content[0]?.text).toContain('Invalid ID format');
|
|
219
|
+
});
|
|
220
|
+
it('should return error response for invalid type', async () => {
|
|
221
|
+
const result = await fetch.execute({ id: 'section:123' }, mockTodoistApi);
|
|
222
|
+
expect(result.isError).toBe(true);
|
|
223
|
+
expect(result.content[0]?.text).toContain('Invalid ID format');
|
|
224
|
+
});
|
|
225
|
+
it('should return error response for task fetch failure', async () => {
|
|
226
|
+
mockTodoistApi.getTask.mockRejectedValue(new Error('Task not found'));
|
|
227
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
228
|
+
expect(result.isError).toBe(true);
|
|
229
|
+
expect(result.content[0]?.text).toBe('Task not found');
|
|
230
|
+
});
|
|
231
|
+
it('should return error response for project fetch failure', async () => {
|
|
232
|
+
mockTodoistApi.getProject.mockRejectedValue(new Error('Project not found'));
|
|
233
|
+
const result = await fetch.execute({ id: `project:${TEST_IDS.PROJECT_WORK}` }, mockTodoistApi);
|
|
234
|
+
expect(result.isError).toBe(true);
|
|
235
|
+
expect(result.content[0]?.text).toBe('Project not found');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe('OpenAI MCP spec compliance', () => {
|
|
239
|
+
it('should return exactly one content item with type "text"', async () => {
|
|
240
|
+
const mockTask = createMockTask({ id: TEST_IDS.TASK_1, content: 'Test' });
|
|
241
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
242
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
243
|
+
expect(result.content).toHaveLength(1);
|
|
244
|
+
expect(result.content[0]?.type).toBe('text');
|
|
245
|
+
});
|
|
246
|
+
it('should return valid JSON string in text field', async () => {
|
|
247
|
+
const mockTask = createMockTask({ id: TEST_IDS.TASK_1, content: 'Test' });
|
|
248
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
249
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
250
|
+
expect(() => JSON.parse(result.content[0]?.text ?? '{}')).not.toThrow();
|
|
251
|
+
});
|
|
252
|
+
it('should include all required fields (id, title, text, url)', async () => {
|
|
253
|
+
const mockTask = createMockTask({ id: TEST_IDS.TASK_1, content: 'Test' });
|
|
254
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
255
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
256
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
257
|
+
expect(jsonResponse).toHaveProperty('id');
|
|
258
|
+
expect(jsonResponse).toHaveProperty('title');
|
|
259
|
+
expect(jsonResponse).toHaveProperty('text');
|
|
260
|
+
expect(jsonResponse).toHaveProperty('url');
|
|
261
|
+
expect(typeof jsonResponse.id).toBe('string');
|
|
262
|
+
expect(typeof jsonResponse.title).toBe('string');
|
|
263
|
+
expect(typeof jsonResponse.text).toBe('string');
|
|
264
|
+
expect(typeof jsonResponse.url).toBe('string');
|
|
265
|
+
});
|
|
266
|
+
it('should include optional metadata field', async () => {
|
|
267
|
+
const mockTask = createMockTask({ id: TEST_IDS.TASK_1, content: 'Test' });
|
|
268
|
+
mockTodoistApi.getTask.mockResolvedValue(mockTask);
|
|
269
|
+
const result = await fetch.execute({ id: `task:${TEST_IDS.TASK_1}` }, mockTodoistApi);
|
|
270
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
271
|
+
expect(jsonResponse).toHaveProperty('metadata');
|
|
272
|
+
expect(typeof jsonResponse.metadata).toBe('object');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/search.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
+
import { createMappedTask, createMockApiResponse, createMockProject, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
4
|
+
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
|
+
import { search } from '../search.js';
|
|
6
|
+
jest.mock('../../tool-helpers', () => {
|
|
7
|
+
const actual = jest.requireActual('../../tool-helpers');
|
|
8
|
+
return {
|
|
9
|
+
getTasksByFilter: jest.fn(),
|
|
10
|
+
buildTodoistUrl: actual.buildTodoistUrl,
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
const { SEARCH } = ToolNames;
|
|
14
|
+
const mockGetTasksByFilter = getTasksByFilter;
|
|
15
|
+
// Mock the Todoist API
|
|
16
|
+
const mockTodoistApi = {
|
|
17
|
+
getProjects: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
describe(`${SEARCH} tool`, () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
describe('searching tasks and projects', () => {
|
|
24
|
+
it('should search both tasks and projects and return combined results', async () => {
|
|
25
|
+
const mockTasks = [
|
|
26
|
+
createMappedTask({
|
|
27
|
+
id: TEST_IDS.TASK_1,
|
|
28
|
+
content: 'Important meeting task',
|
|
29
|
+
}),
|
|
30
|
+
createMappedTask({
|
|
31
|
+
id: TEST_IDS.TASK_2,
|
|
32
|
+
content: 'Another important item',
|
|
33
|
+
}),
|
|
34
|
+
];
|
|
35
|
+
const mockProjects = [
|
|
36
|
+
createMockProject({
|
|
37
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
38
|
+
name: 'Important Work Project',
|
|
39
|
+
}),
|
|
40
|
+
createMockProject({
|
|
41
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
42
|
+
name: 'Test Project',
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
45
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: mockTasks, nextCursor: null });
|
|
46
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
47
|
+
const result = await search.execute({ query: 'important' }, mockTodoistApi);
|
|
48
|
+
// Verify both API calls were made
|
|
49
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
50
|
+
client: mockTodoistApi,
|
|
51
|
+
query: 'search: important',
|
|
52
|
+
limit: 100, // TASKS_MAX
|
|
53
|
+
cursor: undefined,
|
|
54
|
+
});
|
|
55
|
+
expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({
|
|
56
|
+
limit: 100, // PROJECTS_MAX
|
|
57
|
+
});
|
|
58
|
+
// Verify result structure
|
|
59
|
+
expect(result.content).toHaveLength(1);
|
|
60
|
+
expect(result.content[0]?.type).toBe('text');
|
|
61
|
+
// Parse the JSON response
|
|
62
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
63
|
+
expect(jsonResponse).toHaveProperty('results');
|
|
64
|
+
expect(jsonResponse.results).toHaveLength(3); // 2 tasks + 1 project matching "important"
|
|
65
|
+
// Verify task results
|
|
66
|
+
expect(jsonResponse.results[0]).toEqual({
|
|
67
|
+
id: `task:${TEST_IDS.TASK_1}`,
|
|
68
|
+
title: 'Important meeting task',
|
|
69
|
+
url: `https://app.todoist.com/app/task/${TEST_IDS.TASK_1}`,
|
|
70
|
+
});
|
|
71
|
+
expect(jsonResponse.results[1]).toEqual({
|
|
72
|
+
id: `task:${TEST_IDS.TASK_2}`,
|
|
73
|
+
title: 'Another important item',
|
|
74
|
+
url: `https://app.todoist.com/app/task/${TEST_IDS.TASK_2}`,
|
|
75
|
+
});
|
|
76
|
+
// Verify project result (only "Important Work Project" matches)
|
|
77
|
+
expect(jsonResponse.results[2]).toEqual({
|
|
78
|
+
id: `project:${TEST_IDS.PROJECT_WORK}`,
|
|
79
|
+
title: 'Important Work Project',
|
|
80
|
+
url: `https://app.todoist.com/app/project/${TEST_IDS.PROJECT_WORK}`,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it('should return only matching tasks when no projects match', async () => {
|
|
84
|
+
const mockTasks = [
|
|
85
|
+
createMappedTask({
|
|
86
|
+
id: TEST_IDS.TASK_1,
|
|
87
|
+
content: 'Unique task content',
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
const mockProjects = [
|
|
91
|
+
createMockProject({
|
|
92
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
93
|
+
name: 'Work Project',
|
|
94
|
+
}),
|
|
95
|
+
];
|
|
96
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: mockTasks, nextCursor: null });
|
|
97
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
98
|
+
const result = await search.execute({ query: 'unique' }, mockTodoistApi);
|
|
99
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
100
|
+
expect(jsonResponse.results).toHaveLength(1);
|
|
101
|
+
expect(jsonResponse.results[0].id).toBe(`task:${TEST_IDS.TASK_1}`);
|
|
102
|
+
});
|
|
103
|
+
it('should return only matching projects when no tasks match', async () => {
|
|
104
|
+
const mockProjects = [
|
|
105
|
+
createMockProject({
|
|
106
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
107
|
+
name: 'Special Project Name',
|
|
108
|
+
}),
|
|
109
|
+
createMockProject({
|
|
110
|
+
id: TEST_IDS.PROJECT_TEST,
|
|
111
|
+
name: 'Another Project',
|
|
112
|
+
}),
|
|
113
|
+
];
|
|
114
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
115
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
116
|
+
const result = await search.execute({ query: 'special' }, mockTodoistApi);
|
|
117
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
118
|
+
expect(jsonResponse.results).toHaveLength(1);
|
|
119
|
+
expect(jsonResponse.results[0]).toEqual({
|
|
120
|
+
id: `project:${TEST_IDS.PROJECT_WORK}`,
|
|
121
|
+
title: 'Special Project Name',
|
|
122
|
+
url: `https://app.todoist.com/app/project/${TEST_IDS.PROJECT_WORK}`,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it('should return empty results when nothing matches', async () => {
|
|
126
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
127
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
|
|
128
|
+
const result = await search.execute({ query: 'nonexistent' }, mockTodoistApi);
|
|
129
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
130
|
+
expect(jsonResponse.results).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
it('should perform case-insensitive project filtering', async () => {
|
|
133
|
+
const mockProjects = [
|
|
134
|
+
createMockProject({
|
|
135
|
+
id: TEST_IDS.PROJECT_WORK,
|
|
136
|
+
name: 'Important Work',
|
|
137
|
+
}),
|
|
138
|
+
];
|
|
139
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
140
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
141
|
+
const result = await search.execute({ query: 'IMPORTANT' }, mockTodoistApi);
|
|
142
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
143
|
+
expect(jsonResponse.results).toHaveLength(1);
|
|
144
|
+
expect(jsonResponse.results[0].title).toBe('Important Work');
|
|
145
|
+
});
|
|
146
|
+
it('should handle partial matches in project names', async () => {
|
|
147
|
+
const mockProjects = [
|
|
148
|
+
createMockProject({ id: 'project-1', name: 'Development Tasks' }),
|
|
149
|
+
createMockProject({ id: 'project-2', name: 'Developer Resources' }),
|
|
150
|
+
createMockProject({ id: 'project-3', name: 'Marketing' }),
|
|
151
|
+
];
|
|
152
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
153
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
154
|
+
const result = await search.execute({ query: 'develop' }, mockTodoistApi);
|
|
155
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
156
|
+
expect(jsonResponse.results).toHaveLength(2);
|
|
157
|
+
expect(jsonResponse.results[0].title).toBe('Development Tasks');
|
|
158
|
+
expect(jsonResponse.results[1].title).toBe('Developer Resources');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('error handling', () => {
|
|
162
|
+
it('should return error response for task search failure', async () => {
|
|
163
|
+
mockGetTasksByFilter.mockRejectedValue(new Error('Task search failed'));
|
|
164
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
|
|
165
|
+
const result = await search.execute({ query: 'test' }, mockTodoistApi);
|
|
166
|
+
expect(result.isError).toBe(true);
|
|
167
|
+
expect(result.content[0]?.text).toBe('Task search failed');
|
|
168
|
+
});
|
|
169
|
+
it('should return error response for project search failure', async () => {
|
|
170
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
171
|
+
mockTodoistApi.getProjects.mockRejectedValue(new Error('Project search failed'));
|
|
172
|
+
const result = await search.execute({ query: 'test' }, mockTodoistApi);
|
|
173
|
+
expect(result.isError).toBe(true);
|
|
174
|
+
expect(result.content[0]?.text).toBe('Project search failed');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('OpenAI MCP spec compliance', () => {
|
|
178
|
+
it('should return exactly one content item with type "text"', async () => {
|
|
179
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
180
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
|
|
181
|
+
const result = await search.execute({ query: 'test' }, mockTodoistApi);
|
|
182
|
+
expect(result.content).toHaveLength(1);
|
|
183
|
+
expect(result.content[0]?.type).toBe('text');
|
|
184
|
+
});
|
|
185
|
+
it('should return valid JSON string in text field', async () => {
|
|
186
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
|
|
187
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
|
|
188
|
+
const result = await search.execute({ query: 'test' }, mockTodoistApi);
|
|
189
|
+
expect(() => JSON.parse(result.content[0]?.text ?? '{}')).not.toThrow();
|
|
190
|
+
});
|
|
191
|
+
it('should include required fields (id, title, url) in each result', async () => {
|
|
192
|
+
const mockTasks = [createMappedTask({ id: TEST_IDS.TASK_1, content: 'Test' })];
|
|
193
|
+
const mockProjects = [createMockProject({ id: TEST_IDS.PROJECT_WORK, name: 'Test' })];
|
|
194
|
+
mockGetTasksByFilter.mockResolvedValue({ tasks: mockTasks, nextCursor: null });
|
|
195
|
+
mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
|
|
196
|
+
const result = await search.execute({ query: 'test' }, mockTodoistApi);
|
|
197
|
+
const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
|
|
198
|
+
for (const item of jsonResponse.results) {
|
|
199
|
+
expect(item).toHaveProperty('id');
|
|
200
|
+
expect(item).toHaveProperty('title');
|
|
201
|
+
expect(item).toHaveProperty('url');
|
|
202
|
+
expect(typeof item.id).toBe('string');
|
|
203
|
+
expect(typeof item.title).toBe('string');
|
|
204
|
+
expect(typeof item.url).toBe('string');
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/add-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAClF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"add-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/add-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAClF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqDvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
package/dist/tools/add-tasks.js
CHANGED
|
@@ -7,8 +7,14 @@ import { convertPriorityToNumber, PrioritySchema } from '../utils/priorities.js'
|
|
|
7
7
|
import { generateTaskNextSteps, getDateString, summarizeTaskOperation, } from '../utils/response-builders.js';
|
|
8
8
|
import { ToolNames } from '../utils/tool-names.js';
|
|
9
9
|
const TaskSchema = z.object({
|
|
10
|
-
content: z
|
|
11
|
-
|
|
10
|
+
content: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe('The task name/title. Should be concise and actionable (e.g., "Review PR #123", "Call dentist"). For longer content, use the description field instead. Supports Markdown.'),
|
|
14
|
+
description: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Additional details, notes, or context for the task. Use this for longer content rather than putting it in the task name. Supports Markdown.'),
|
|
12
18
|
priority: PrioritySchema.optional().describe('The priority of the task: p1 (highest), p2 (high), p3 (medium), p4 (lowest/default).'),
|
|
13
19
|
dueString: z.string().optional().describe('The due date for the task, in natural language.'),
|
|
14
20
|
duration: z
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
type FetchToolOutput = {
|
|
3
|
+
content: {
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
|
|
11
|
+
*
|
|
12
|
+
* This tool follows the OpenAI MCP fetch tool specification:
|
|
13
|
+
* @see https://platform.openai.com/docs/mcp#fetch-tool
|
|
14
|
+
*/
|
|
15
|
+
declare const fetch: {
|
|
16
|
+
name: "fetch";
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: {
|
|
19
|
+
id: z.ZodString;
|
|
20
|
+
};
|
|
21
|
+
execute(args: {
|
|
22
|
+
id: string;
|
|
23
|
+
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<FetchToolOutput>;
|
|
24
|
+
};
|
|
25
|
+
export { fetch };
|
|
26
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/tools/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAuBvB,KAAK,eAAe,GAAG;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,QAAA,MAAM,KAAK;;;;;;;;oEAKsB,OAAO,CAAC,eAAe,CAAC;CAsFf,CAAA;AAE1C,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getErrorOutput } from '../mcp-helpers.js';
|
|
3
|
+
import { buildTodoistUrl, mapProject, mapTask } from '../tool-helpers.js';
|
|
4
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
5
|
+
const ArgsSchema = {
|
|
6
|
+
id: z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.describe('A unique identifier for the document in the format "task:{id}" or "project:{id}".'),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
|
|
13
|
+
*
|
|
14
|
+
* This tool follows the OpenAI MCP fetch tool specification:
|
|
15
|
+
* @see https://platform.openai.com/docs/mcp#fetch-tool
|
|
16
|
+
*/
|
|
17
|
+
const fetch = {
|
|
18
|
+
name: ToolNames.FETCH,
|
|
19
|
+
description: 'Fetch the full contents of a task or project by its ID. The ID should be in the format "task:{id}" or "project:{id}".',
|
|
20
|
+
parameters: ArgsSchema,
|
|
21
|
+
async execute(args, client) {
|
|
22
|
+
try {
|
|
23
|
+
const { id } = args;
|
|
24
|
+
// Parse the composite ID
|
|
25
|
+
const [type, objectId] = id.split(':', 2);
|
|
26
|
+
if (!objectId || (type !== 'task' && type !== 'project')) {
|
|
27
|
+
throw new Error('Invalid ID format. Expected "task:{id}" or "project:{id}". Example: "task:8485093748" or "project:6cfCcrrCFg2xP94Q"');
|
|
28
|
+
}
|
|
29
|
+
let result;
|
|
30
|
+
if (type === 'task') {
|
|
31
|
+
// Fetch task
|
|
32
|
+
const task = await client.getTask(objectId);
|
|
33
|
+
const mappedTask = mapTask(task);
|
|
34
|
+
// Build text content
|
|
35
|
+
const textParts = [mappedTask.content];
|
|
36
|
+
if (mappedTask.description) {
|
|
37
|
+
textParts.push(`\n\nDescription: ${mappedTask.description}`);
|
|
38
|
+
}
|
|
39
|
+
if (mappedTask.dueDate) {
|
|
40
|
+
textParts.push(`\nDue: ${mappedTask.dueDate}`);
|
|
41
|
+
}
|
|
42
|
+
if (mappedTask.labels.length > 0) {
|
|
43
|
+
textParts.push(`\nLabels: ${mappedTask.labels.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
result = {
|
|
46
|
+
id: `task:${mappedTask.id}`,
|
|
47
|
+
title: mappedTask.content,
|
|
48
|
+
text: textParts.join(''),
|
|
49
|
+
url: buildTodoistUrl('task', mappedTask.id),
|
|
50
|
+
metadata: {
|
|
51
|
+
priority: mappedTask.priority,
|
|
52
|
+
projectId: mappedTask.projectId,
|
|
53
|
+
sectionId: mappedTask.sectionId,
|
|
54
|
+
parentId: mappedTask.parentId,
|
|
55
|
+
recurring: mappedTask.recurring,
|
|
56
|
+
duration: mappedTask.duration,
|
|
57
|
+
responsibleUid: mappedTask.responsibleUid,
|
|
58
|
+
assignedByUid: mappedTask.assignedByUid,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Fetch project
|
|
64
|
+
const project = await client.getProject(objectId);
|
|
65
|
+
const mappedProject = mapProject(project);
|
|
66
|
+
// Build text content
|
|
67
|
+
const textParts = [mappedProject.name];
|
|
68
|
+
if (mappedProject.isShared) {
|
|
69
|
+
textParts.push('\n\nShared project');
|
|
70
|
+
}
|
|
71
|
+
if (mappedProject.isFavorite) {
|
|
72
|
+
textParts.push('\nFavorite: Yes');
|
|
73
|
+
}
|
|
74
|
+
result = {
|
|
75
|
+
id: `project:${mappedProject.id}`,
|
|
76
|
+
title: mappedProject.name,
|
|
77
|
+
text: textParts.join(''),
|
|
78
|
+
url: buildTodoistUrl('project', mappedProject.id),
|
|
79
|
+
metadata: {
|
|
80
|
+
color: mappedProject.color,
|
|
81
|
+
isFavorite: mappedProject.isFavorite,
|
|
82
|
+
isShared: mappedProject.isShared,
|
|
83
|
+
parentId: mappedProject.parentId,
|
|
84
|
+
inboxProject: mappedProject.inboxProject,
|
|
85
|
+
viewStyle: mappedProject.viewStyle,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Return as JSON-encoded string in a text content item (OpenAI MCP spec)
|
|
90
|
+
const jsonText = JSON.stringify(result);
|
|
91
|
+
return { content: [{ type: 'text', text: jsonText }] };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
95
|
+
return getErrorOutput(message);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
export { fetch };
|
|
@@ -9,8 +9,8 @@ declare const findProjects: {
|
|
|
9
9
|
};
|
|
10
10
|
execute(args: {
|
|
11
11
|
limit: number;
|
|
12
|
-
cursor?: string | undefined;
|
|
13
12
|
search?: string | undefined;
|
|
13
|
+
cursor?: string | undefined;
|
|
14
14
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
15
15
|
content: {
|
|
16
16
|
type: "text";
|
|
@@ -32,8 +32,8 @@ declare const findProjects: {
|
|
|
32
32
|
hasMore: boolean;
|
|
33
33
|
appliedFilters: {
|
|
34
34
|
limit: number;
|
|
35
|
-
cursor?: string | undefined;
|
|
36
35
|
search?: string | undefined;
|
|
36
|
+
cursor?: string | undefined;
|
|
37
37
|
};
|
|
38
38
|
};
|
|
39
39
|
} | {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
type SearchToolOutput = {
|
|
3
|
+
content: {
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* OpenAI MCP search tool - returns a list of relevant search results from Todoist.
|
|
11
|
+
*
|
|
12
|
+
* This tool follows the OpenAI MCP search tool specification:
|
|
13
|
+
* @see https://platform.openai.com/docs/mcp#search-tool
|
|
14
|
+
*/
|
|
15
|
+
declare const search: {
|
|
16
|
+
name: "search";
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: {
|
|
19
|
+
query: z.ZodString;
|
|
20
|
+
};
|
|
21
|
+
execute(args: {
|
|
22
|
+
query: string;
|
|
23
|
+
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<SearchToolOutput>;
|
|
24
|
+
};
|
|
25
|
+
export { search };
|
|
26
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,KAAK,gBAAgB,GAAG;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,QAAA,MAAM,MAAM;;;;;;;;oEAKqB,OAAO,CAAC,gBAAgB,CAAC;CAmDhB,CAAA;AAE1C,OAAO,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getErrorOutput } from '../mcp-helpers.js';
|
|
3
|
+
import { buildTodoistUrl, getTasksByFilter } from '../tool-helpers.js';
|
|
4
|
+
import { ApiLimits } from '../utils/constants.js';
|
|
5
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
6
|
+
const ArgsSchema = {
|
|
7
|
+
query: z.string().min(1).describe('The search query string to find tasks and projects.'),
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* OpenAI MCP search tool - returns a list of relevant search results from Todoist.
|
|
11
|
+
*
|
|
12
|
+
* This tool follows the OpenAI MCP search tool specification:
|
|
13
|
+
* @see https://platform.openai.com/docs/mcp#search-tool
|
|
14
|
+
*/
|
|
15
|
+
const search = {
|
|
16
|
+
name: ToolNames.SEARCH,
|
|
17
|
+
description: 'Search across tasks and projects in Todoist. Returns a list of relevant results with IDs, titles, and URLs.',
|
|
18
|
+
parameters: ArgsSchema,
|
|
19
|
+
async execute(args, client) {
|
|
20
|
+
try {
|
|
21
|
+
const { query } = args;
|
|
22
|
+
// Search both tasks and projects in parallel
|
|
23
|
+
// Use TASKS_MAX for search since this tool doesn't support pagination
|
|
24
|
+
const [tasksResult, projectsResponse] = await Promise.all([
|
|
25
|
+
getTasksByFilter({
|
|
26
|
+
client,
|
|
27
|
+
query: `search: ${query}`,
|
|
28
|
+
limit: ApiLimits.TASKS_MAX,
|
|
29
|
+
cursor: undefined,
|
|
30
|
+
}),
|
|
31
|
+
client.getProjects({ limit: ApiLimits.PROJECTS_MAX }),
|
|
32
|
+
]);
|
|
33
|
+
// Filter projects by search query (case-insensitive)
|
|
34
|
+
const searchLower = query.toLowerCase();
|
|
35
|
+
const matchingProjects = projectsResponse.results.filter((project) => project.name.toLowerCase().includes(searchLower));
|
|
36
|
+
// Build results array
|
|
37
|
+
const results = [];
|
|
38
|
+
// Add task results with composite IDs
|
|
39
|
+
for (const task of tasksResult.tasks) {
|
|
40
|
+
results.push({
|
|
41
|
+
id: `task:${task.id}`,
|
|
42
|
+
title: task.content,
|
|
43
|
+
url: buildTodoistUrl('task', task.id),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Add project results with composite IDs
|
|
47
|
+
for (const project of matchingProjects) {
|
|
48
|
+
results.push({
|
|
49
|
+
id: `project:${project.id}`,
|
|
50
|
+
title: project.name,
|
|
51
|
+
url: buildTodoistUrl('project', project.id),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Return as JSON-encoded string in a text content item (OpenAI MCP spec)
|
|
55
|
+
const jsonText = JSON.stringify({ results });
|
|
56
|
+
return { content: [{ type: 'text', text: jsonText }] };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
60
|
+
return getErrorOutput(message);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
export { search };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/update-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"update-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/update-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA8DvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkHyB,CAAA;AAqC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -9,8 +9,14 @@ import { ToolNames } from '../utils/tool-names.js';
|
|
|
9
9
|
const { FIND_TASKS_BY_DATE, GET_OVERVIEW } = ToolNames;
|
|
10
10
|
const TasksUpdateSchema = z.object({
|
|
11
11
|
id: z.string().min(1).describe('The ID of the task to update.'),
|
|
12
|
-
content: z
|
|
13
|
-
|
|
12
|
+
content: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('The new task name/title. Should be concise and actionable (e.g., "Review PR #123", "Call dentist"). For longer content, use the description field instead. Supports Markdown.'),
|
|
16
|
+
description: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('New additional details, notes, or context for the task. Use this for longer content rather than putting it in the task name. Supports Markdown.'),
|
|
14
20
|
projectId: z.string().optional().describe('The new project ID for the task.'),
|
|
15
21
|
sectionId: z.string().optional().describe('The new section ID for the task.'),
|
|
16
22
|
parentId: z.string().optional().describe('The new parent task ID (for subtasks).'),
|
|
@@ -8,7 +8,7 @@ export declare const ApiLimits: {
|
|
|
8
8
|
/** Default limit for task listings */
|
|
9
9
|
readonly TASKS_DEFAULT: 10;
|
|
10
10
|
/** Maximum limit for task search and list operations */
|
|
11
|
-
readonly TASKS_MAX:
|
|
11
|
+
readonly TASKS_MAX: 100;
|
|
12
12
|
/** Default limit for completed tasks */
|
|
13
13
|
readonly COMPLETED_TASKS_DEFAULT: 50;
|
|
14
14
|
/** Maximum limit for completed tasks */
|
package/dist/utils/constants.js
CHANGED
|
@@ -9,7 +9,7 @@ export const ApiLimits = {
|
|
|
9
9
|
/** Default limit for task listings */
|
|
10
10
|
TASKS_DEFAULT: 10,
|
|
11
11
|
/** Maximum limit for task search and list operations */
|
|
12
|
-
TASKS_MAX:
|
|
12
|
+
TASKS_MAX: 100,
|
|
13
13
|
/** Default limit for completed tasks */
|
|
14
14
|
COMPLETED_TASKS_DEFAULT: 50,
|
|
15
15
|
/** Maximum limit for completed tasks */
|
|
@@ -26,6 +26,8 @@ export declare const ToolNames: {
|
|
|
26
26
|
readonly GET_OVERVIEW: "get-overview";
|
|
27
27
|
readonly DELETE_OBJECT: "delete-object";
|
|
28
28
|
readonly USER_INFO: "user-info";
|
|
29
|
+
readonly SEARCH: "search";
|
|
30
|
+
readonly FETCH: "fetch";
|
|
29
31
|
};
|
|
30
32
|
export type ToolName = (typeof ToolNames)[keyof typeof ToolNames];
|
|
31
33
|
//# sourceMappingURL=tool-names.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../../src/utils/tool-names.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS
|
|
1
|
+
{"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../../src/utils/tool-names.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;CAoCZ,CAAA;AAGV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAA"}
|
package/dist/utils/tool-names.js
CHANGED