@jorgeluismlima/teamwork-mcp 1.0.1 → 1.0.2

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.
Files changed (3) hide show
  1. package/README.md +7 -2
  2. package/dist/index.js +274 -29
  3. package/package.json +11 -3
package/README.md CHANGED
@@ -7,14 +7,19 @@ A Model Context Protocol (MCP) server that connects to the Teamwork API, allowin
7
7
  This server exposes the following tools:
8
8
 
9
9
  ### Projects & Time Tracking
10
- * **`list_projects`**: Listings projects, filtering by search term.
10
+ * **`list_projects`**: Comprehensive project listing with 50+ filters (date, health, status, tags, etc.) and full API response support.
11
11
  * **`create_time_entry`**: Creates a time entry (timelog) for a specific project.
12
- * **`get_time_entries`**: Retrieves time entries for a project, with optional date filtering.
12
+ * **`get_time_entries`**: Retrieves detailed time entries with 60+ filters (billed status, users, tags, etc.).
13
13
  * **`total_count_of_active_projects`**: Returns the total count of active projects.
14
14
  * **`total_billable_time_per_project`**: Returns total billable minutes per project (optional date range).
15
15
  * **`get_health_stats`**: Returns project health metrics (Good, Bad, OK, etc.), with various filters.
16
16
  * **`get_project_time_totals`**: Returns total hours/minutes for a project (optionally filtered by user).
17
17
 
18
+ ### Project Categories
19
+ * **`list_of_project_categories`**: Lists all project categories with filtering options.
20
+ * **`get_specific_project_category`**: Retrieves details of a specific category.
21
+ * **`list_of_teamwork_project_categories`**: Helper tool to list categories.
22
+
18
23
  ### People & Companies
19
24
  * **`get_project_people`**: Lists all people associated with a project.
20
25
  * **`get_person`**: Retrieves detailed information about a specific person (user).
package/dist/index.js CHANGED
@@ -57,30 +57,105 @@ const handleApiError = (error) => {
57
57
  };
58
58
  };
59
59
  server.tool("list_projects", {
60
- searchTerm: z.string().optional().describe("Filter by project name"),
61
- page: z.number().optional().describe("Page number"),
62
- }, async ({ searchTerm, page }) => {
60
+ updatedAfter: z.string().optional().describe("updated after"),
61
+ timeMode: z.enum(["timelogs", "estimated"]).optional().describe("profitability time mode"),
62
+ searchTerm: z.string().optional().describe("filter by project name"),
63
+ reportType: z.enum(["project", "health"]).optional().describe("define the type of the report"),
64
+ reportTimezone: z.string().optional().describe("Optional to configure the report dates displayed in a timezone"),
65
+ reportFormat: z.enum(["csv", "html", "pdf", "xls"]).optional().describe("define the format of the report"),
66
+ projectType: z.string().optional().describe("filter by project type"),
67
+ orderMode: z.enum(["asc", "desc"]).optional().describe("order mode"),
68
+ orderBy: z.enum(["companyname", "datecreated", "duedate", "lastactivity", "name", "namecaseinsensitive", "ownercompany", "starred", "categoryname"]).optional().describe("order by"),
69
+ notCompletedBefore: z.string().optional().describe("filter by projects that have not been completed before the given date"),
70
+ minLastActivityDate: z.string().optional().describe("filter by min last activity date"),
71
+ maxLastActivityDate: z.string().optional().describe("filter by max last activity date"),
72
+ userId: z.number().optional().describe("filter by user id"),
73
+ pageSize: z.number().optional().describe("number of items in a page"),
74
+ page: z.number().optional().describe("page number"),
75
+ orderByCustomFieldId: z.number().optional().describe("order by custom field id when orderBy is equal to customfield"),
76
+ minBudgetCapacityUsedPercent: z.number().optional().describe("filter by minimum budget capacity used"),
77
+ maxBudgetCapacityUsedPercent: z.number().optional().describe("filter by maximum budget capacity used"),
78
+ useFormulaFields: z.boolean().optional().describe("use formula fields"),
79
+ skipCounts: z.boolean().optional().describe("SkipCounts allows you to skip doing counts on a list API endpoint"),
80
+ searchCompanies: z.boolean().optional().describe("include companies in the search"),
81
+ searchByLetter: z.boolean().optional().describe("search projects beginning with the search term character only"),
82
+ onlyStarredProjects: z.boolean().optional().describe("filter by starred projects only"),
83
+ onlyProjectsWithExplicitMembership: z.boolean().optional().describe("only show projects with explicit membership"),
84
+ onlyProjectsThatCanLogTime: z.boolean().optional().describe("can log time on projects"),
85
+ onlyProjectsThatCanAddTasks: z.boolean().optional().describe("can add tasks on projects"),
86
+ onlyArchivedProjects: z.boolean().optional().describe("return only archived projects"),
87
+ matchAllProjectTags: z.boolean().optional().describe("match all project tags"),
88
+ matchAllExcludedTags: z.boolean().optional().describe("match all excluded project tags"),
89
+ isReportDownload: z.boolean().optional().describe("generate a report document"),
90
+ includeTentativeProjects: z.boolean().optional().describe("include alongside normal projects, tentative ones"),
91
+ includeTabSystemStatus: z.boolean().optional().describe("include tab system status in response"),
92
+ includeSubCategories: z.boolean().optional().describe("include sub categories when filtering by ids"),
93
+ includeStats: z.boolean().optional().describe("include project status counts"),
94
+ includeProjectUserInfo: z.boolean().optional().describe("fetch user-specific data such as isStarred"),
95
+ includeProjectProfitability: z.boolean().optional().describe("include project profitability in response"),
96
+ includeProjectDates: z.boolean().optional().describe("include minimum and maximum start/end dates for projects"),
97
+ includeCustomFields: z.boolean().optional().describe("include custom fields"),
98
+ includeCounts: z.boolean().optional().describe("include project related counts"),
99
+ includeCompletedStatus: z.boolean().optional().describe("include completed projects when filtering by project statuses current,late"),
100
+ includeArchivedProjects: z.boolean().optional().describe("include archived projects"),
101
+ hideObservedProjects: z.boolean().optional().describe("hide projects where the logged-in user is just an observer"),
102
+ alwaysIncludeFiltering: z.boolean().optional().describe("includes filters when project ids are provided"),
103
+ usersWithExplicitMembershipIds: z.array(z.number()).optional().describe("only show projects that have an explicit common membership with provided user ids"),
104
+ teamIds: z.array(z.number()).optional().describe("filter by projects that contain users associated with the team ids"),
105
+ selectedColumns: z.array(z.string()).optional().describe("select the columns to use in exports"),
106
+ projectTagIds: z.array(z.number()).optional().describe("filter by project tag ids"),
107
+ projectStatuses: z.array(z.string()).optional().describe("filter by project status (active, current, late, upcoming, completed, deleted)"),
108
+ projectOwnerIds: z.array(z.number()).optional().describe("filter by project owner ids"),
109
+ projectIds: z.array(z.number()).optional().describe("filter by project ids"),
110
+ projectHealths: z.array(z.number()).optional().describe("filter by project healths (0: not set, 1: bad, 2: ok, 3: good)"),
111
+ projectCompanyIds: z.array(z.number()).optional().describe("filter by company ids"),
112
+ projectCategoryIds: z.array(z.number()).optional().describe("filter by project category ids"),
113
+ includeCustomFieldIds: z.array(z.number()).optional().describe("include specific custom fields"),
114
+ include: z.array(z.string()).optional().describe("include related data"),
115
+ featuresEnabled: z.array(z.string()).optional().describe("filter by projects that have features enabled"),
116
+ excludeTagIds: z.array(z.number()).optional().describe("exclude by project tag ids"),
117
+ excludeProjectIds: z.array(z.number()).optional().describe("exclude certain project ids"),
118
+ }, async (args) => {
63
119
  try {
64
- const params = {};
65
- if (searchTerm)
66
- params.searchTerm = searchTerm;
67
- if (page)
68
- params.page = page;
120
+ const params = { ...args };
121
+ // Helper to join arrays
122
+ const joinParam = (arr) => arr ? arr.join(',') : undefined;
123
+ if (args.usersWithExplicitMembershipIds)
124
+ params.usersWithExplicitMembershipIds = joinParam(args.usersWithExplicitMembershipIds);
125
+ if (args.teamIds)
126
+ params.teamIds = joinParam(args.teamIds);
127
+ if (args.selectedColumns)
128
+ params.selectedColumns = joinParam(args.selectedColumns);
129
+ if (args.projectTagIds)
130
+ params.projectTagIds = joinParam(args.projectTagIds);
131
+ if (args.projectStatuses)
132
+ params.projectStatuses = joinParam(args.projectStatuses);
133
+ if (args.projectOwnerIds)
134
+ params.projectOwnerIds = joinParam(args.projectOwnerIds);
135
+ if (args.projectIds)
136
+ params.projectIds = joinParam(args.projectIds);
137
+ if (args.projectHealths)
138
+ params.projectHealths = joinParam(args.projectHealths);
139
+ if (args.projectCompanyIds)
140
+ params.projectCompanyIds = joinParam(args.projectCompanyIds);
141
+ if (args.projectCategoryIds)
142
+ params.projectCategoryIds = joinParam(args.projectCategoryIds);
143
+ if (args.includeCustomFieldIds)
144
+ params.includeCustomFieldIds = joinParam(args.includeCustomFieldIds);
145
+ if (args.include)
146
+ params.include = joinParam(args.include);
147
+ if (args.featuresEnabled)
148
+ params.featuresEnabled = joinParam(args.featuresEnabled);
149
+ if (args.excludeTagIds)
150
+ params.excludeTagIds = joinParam(args.excludeTagIds);
151
+ if (args.excludeProjectIds)
152
+ params.excludeProjectIds = joinParam(args.excludeProjectIds);
69
153
  const response = await axiosInstance.get("/projects.json", { params });
70
- const simplifiedProjects = response.data.projects.map((p) => ({
71
- id: p.id,
72
- name: p.name,
73
- description: p.description,
74
- status: p.status,
75
- company: p.company,
76
- createdOn: p.createdOn,
77
- lastUpdated: p.lastUpdated
78
- }));
79
154
  return {
80
155
  content: [
81
156
  {
82
157
  type: "text",
83
- text: JSON.stringify({ projects: simplifiedProjects, meta: response.data.meta }, null, 2),
158
+ text: JSON.stringify(response.data, null, 2),
84
159
  },
85
160
  ],
86
161
  };
@@ -129,18 +204,103 @@ server.tool("create_time_entry", {
129
204
  });
130
205
  server.tool("get_time_entries", {
131
206
  projectId: z.number().describe("The ID of the project"),
132
- page: z.number().optional().describe("Page number"),
133
- fromDate: z.string().optional().describe("Start date (YYYYMMDD)"),
134
- toDate: z.string().optional().describe("End date (YYYYMMDD)"),
135
- }, async ({ projectId, page, fromDate, toDate }) => {
207
+ updatedAfter: z.string().optional().describe("filter by updated after date"),
208
+ startDate: z.string().optional().describe("filter by a starting date"),
209
+ reportFormat: z.string().optional().describe("define the format of the report"),
210
+ projectStatus: z.string().optional().describe("filter by project status (active, current, late, upcoming, completed, deleted)"),
211
+ orderMode: z.enum(["asc", "desc"]).optional().describe("order mode"),
212
+ orderBy: z.enum(["company", "date", "dateupdated", "project", "task", "tasklist", "user", "description", "billed", "billable", "timespent"]).optional().describe("sort order"),
213
+ invoicedType: z.enum(["all", "invoiced", "noninvoiced"]).optional().describe("filter by invoiced type"),
214
+ endDate: z.string().optional().describe("filter by an ending date"),
215
+ deletedAfter: z.string().optional().describe("filter by deleted after date"),
216
+ billableType: z.enum(["all", "billable", "non-billable"]).optional().describe("filter by billable type"),
217
+ updatedBy: z.number().optional().describe("filter by the user who updated the timelog"),
218
+ ticketId: z.number().optional().describe("filter by ticket id"),
219
+ tasklistId: z.number().optional().describe("filter by tasklist id"),
220
+ taskId: z.number().optional().describe("filter by task id (deprecated, use taskIds)"),
221
+ pageSize: z.number().optional().describe("number of items in a page"),
222
+ page: z.number().optional().describe("page number"),
223
+ invoiceId: z.number().optional().describe("filter by invoice id"),
224
+ budgetId: z.number().optional().describe("filter by budget id"),
225
+ allocationId: z.number().optional().describe("filter by allocation id"),
226
+ useFallbackMethod: z.boolean().optional().describe("use fallback method"),
227
+ unattachedTimelogs: z.boolean().optional().describe("filter by timelogs that are directly logged against projects"),
228
+ skipCounts: z.boolean().optional().describe("SkipCounts allows you to skip doing counts"),
229
+ showDeleted: z.boolean().optional().describe("include deleted items"),
230
+ returnCostInfo: z.boolean().optional().describe("used to return the cost rate and total"),
231
+ returnBillableInfo: z.boolean().optional().describe("used to return the billable rate and total"),
232
+ onlyStarredProjects: z.boolean().optional().describe("filter by starred projects only"),
233
+ matchAllTaskTags: z.boolean().optional().describe("match all task tags"),
234
+ matchAllTags: z.boolean().optional().describe("match all tags"),
235
+ matchAllProjectTags: z.boolean().optional().describe("match all project tags"),
236
+ isReportDownload: z.boolean().optional().describe("generate a report document"),
237
+ includeTotals: z.boolean().optional().describe("include totals"),
238
+ includeTentativeProjects: z.boolean().optional().describe("include tentative projects"),
239
+ includePermissions: z.boolean().optional().describe("include permissions"),
240
+ includeDescendants: z.boolean().optional().describe("include descendants"),
241
+ includeArchivedProjects: z.boolean().optional().describe("include archived projects"),
242
+ deletedOnly: z.boolean().optional().describe("only deleted tasks"),
243
+ checkLocking: z.boolean().optional().describe("check timesheets' locking"),
244
+ taskTagIds: z.array(z.number()).optional().describe("filter by task tag ids"),
245
+ taskStatuses: z.array(z.string()).optional().describe("filter by task statuses"),
246
+ taskIds: z.array(z.number()).optional().describe("filter by task ids"),
247
+ tagIds: z.array(z.number()).optional().describe("filter by tag ids"),
248
+ selectedColumns: z.array(z.string()).optional().describe("customise the report by selecting columns"),
249
+ projectTagIds: z.array(z.number()).optional().describe("filter by project tag ids"),
250
+ projectStatuses: z.array(z.string()).optional().describe("filter by project statuses"),
251
+ projectOwnerIds: z.array(z.number()).optional().describe("filter by project owner ids"),
252
+ projectIds: z.array(z.number()).optional().describe("filter by project ids"),
253
+ projectHealths: z.array(z.number()).optional().describe("filter by project healths (0: not set, 1: bad, 2: ok, 3: good)"),
254
+ projectCompanyIds: z.array(z.number()).optional().describe("filter by project company ids"),
255
+ projectCategoryIds: z.array(z.number()).optional().describe("filter by project category ids"),
256
+ include: z.array(z.string()).optional().describe("include related data"),
257
+ ids: z.array(z.number()).optional().describe("filter by ids"),
258
+ assignedToUserIds: z.array(z.number()).optional().describe("filter by assigned user ids"),
259
+ assignedToTeamIds: z.array(z.number()).optional().describe("filter by assigned team ids"),
260
+ assignedToCompanyIds: z.array(z.number()).optional().describe("filter by assigned company ids"),
261
+ assignedTeamIds: z.array(z.number()).optional().describe("filter by assigned team ids"),
262
+ }, async (args) => {
136
263
  try {
137
- const params = {};
138
- if (page)
139
- params.page = page;
140
- if (fromDate)
141
- params.fromDate = fromDate;
142
- if (toDate)
143
- params.toDate = toDate;
264
+ const { projectId, ...restParams } = args;
265
+ const params = { ...restParams };
266
+ // Helper to join arrays
267
+ const joinParam = (arr) => arr ? arr.join(',') : undefined;
268
+ if (args.taskTagIds)
269
+ params.taskTagIds = joinParam(args.taskTagIds);
270
+ if (args.taskStatuses)
271
+ params.taskStatuses = joinParam(args.taskStatuses);
272
+ if (args.taskIds)
273
+ params.taskIds = joinParam(args.taskIds);
274
+ if (args.tagIds)
275
+ params.tagIds = joinParam(args.tagIds);
276
+ if (args.selectedColumns)
277
+ params.selectedColumns = joinParam(args.selectedColumns);
278
+ if (args.projectTagIds)
279
+ params.projectTagIds = joinParam(args.projectTagIds);
280
+ if (args.projectStatuses)
281
+ params.projectStatuses = joinParam(args.projectStatuses);
282
+ if (args.projectOwnerIds)
283
+ params.projectOwnerIds = joinParam(args.projectOwnerIds);
284
+ if (args.projectIds)
285
+ params.projectIds = joinParam(args.projectIds);
286
+ if (args.projectHealths)
287
+ params.projectHealths = joinParam(args.projectHealths);
288
+ if (args.projectCompanyIds)
289
+ params.projectCompanyIds = joinParam(args.projectCompanyIds);
290
+ if (args.projectCategoryIds)
291
+ params.projectCategoryIds = joinParam(args.projectCategoryIds);
292
+ if (args.include)
293
+ params.include = joinParam(args.include);
294
+ if (args.ids)
295
+ params.ids = joinParam(args.ids);
296
+ if (args.assignedToUserIds)
297
+ params.assignedToUserIds = joinParam(args.assignedToUserIds);
298
+ if (args.assignedToTeamIds)
299
+ params.assignedToTeamIds = joinParam(args.assignedToTeamIds);
300
+ if (args.assignedToCompanyIds)
301
+ params.assignedToCompanyIds = joinParam(args.assignedToCompanyIds);
302
+ if (args.assignedTeamIds)
303
+ params.assignedTeamIds = joinParam(args.assignedTeamIds);
144
304
  const response = await axiosInstance.get(`/projects/${projectId}/time.json`, { params });
145
305
  return {
146
306
  content: [
@@ -358,6 +518,91 @@ server.tool("get_company", {
358
518
  return handleApiError(error);
359
519
  }
360
520
  });
521
+ server.tool("list_of_project_categories", {
522
+ searchTerm: z.string().optional().describe("Filter by name"),
523
+ onlyStarredProjects: z.boolean().optional().describe("Filter by starred projects only"),
524
+ projectStatuses: z.array(z.string()).optional().describe("Filter by project statuses (Comma separated values)"),
525
+ ids: z.array(z.number()).optional().describe("Filter by specific ids"),
526
+ fields: z.array(z.string()).optional().describe("Fields to return: id, name, color, count, parent, parentId"),
527
+ }, async (args) => {
528
+ try {
529
+ const params = {};
530
+ if (args.searchTerm)
531
+ params.searchTerm = args.searchTerm;
532
+ if (args.onlyStarredProjects !== undefined)
533
+ params.onlyStarredProjects = args.onlyStarredProjects;
534
+ const joinParam = (arr) => arr ? arr.join(',') : undefined;
535
+ if (args.projectStatuses)
536
+ params.projectStatuses = joinParam(args.projectStatuses);
537
+ if (args.ids)
538
+ params.ids = joinParam(args.ids);
539
+ if (args.fields)
540
+ params['fields[projectcategories]'] = joinParam(args.fields);
541
+ const response = await axiosInstance.get("/projectcategories.json", { params });
542
+ return {
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: JSON.stringify(response.data, null, 2),
547
+ },
548
+ ],
549
+ };
550
+ }
551
+ catch (error) {
552
+ return handleApiError(error);
553
+ }
554
+ });
555
+ server.tool("get_specific_project_category", {
556
+ id: z.number().describe("The ID of the category"),
557
+ searchTerm: z.string().optional().describe("Filter by name"),
558
+ onlyStarredProjects: z.boolean().optional().describe("Filter by starred projects only"),
559
+ projectStatuses: z.array(z.string()).optional().describe("Filter by project statuses"),
560
+ ids: z.array(z.number()).optional().describe("Filter by specific ids"),
561
+ fields: z.array(z.string()).optional().describe("Fields to return"),
562
+ }, async (args) => {
563
+ try {
564
+ const params = {};
565
+ if (args.searchTerm)
566
+ params.searchTerm = args.searchTerm;
567
+ if (args.onlyStarredProjects !== undefined)
568
+ params.onlyStarredProjects = args.onlyStarredProjects;
569
+ const joinParam = (arr) => arr ? arr.join(',') : undefined;
570
+ if (args.projectStatuses)
571
+ params.projectStatuses = joinParam(args.projectStatuses);
572
+ if (args.ids)
573
+ params.ids = joinParam(args.ids);
574
+ if (args.fields)
575
+ params['fields[projectcategories]'] = joinParam(args.fields);
576
+ const response = await axiosInstance.get(`/projectcategories/${args.id}.json`, { params });
577
+ return {
578
+ content: [
579
+ {
580
+ type: "text",
581
+ text: JSON.stringify(response.data, null, 2),
582
+ },
583
+ ],
584
+ };
585
+ }
586
+ catch (error) {
587
+ return handleApiError(error);
588
+ }
589
+ });
590
+ server.tool("list_of_teamwork_project_categories", {}, async () => {
591
+ try {
592
+ const response = await axiosInstance.get("/projects/teamwork/categories.json");
593
+ return {
594
+ content: [
595
+ {
596
+ type: "text",
597
+ text: JSON.stringify(response.data, null, 2),
598
+ },
599
+ ],
600
+ };
601
+ }
602
+ catch (error) {
603
+ return handleApiError(error);
604
+ }
605
+ });
361
606
  async function main() {
362
607
  const transport = new StdioServerTransport();
363
608
  await server.connect(transport);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.0.1",
6
+ "version": "1.0.2",
7
7
  "description": "MCP Server for Teamwork",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
@@ -18,8 +18,16 @@
18
18
  "start": "node dist/index.js",
19
19
  "dev": "tsc --watch"
20
20
  },
21
- "keywords": [],
22
- "author": "",
21
+ "keywords": [
22
+ "mcp",
23
+ "teamwork",
24
+ "model-context-protocol",
25
+ "ai",
26
+ "agent",
27
+ "llm",
28
+ "api"
29
+ ],
30
+ "author": "Jorge Luis M. Lima",
23
31
  "license": "ISC",
24
32
  "dependencies": {
25
33
  "@modelcontextprotocol/sdk": "^1.0.1",