systematics-mcp 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +158 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21,35 +21,81 @@ function getToken() {
21
21
  function handleAuthError() {
22
22
  clearStoredToken();
23
23
  api = null;
24
+ authenticated = false;
24
25
  }
25
- // Lazy API client -- initialized on first use or after auth
26
+ // Track auth state
27
+ let authenticated = false;
26
28
  let api = null;
27
29
  const token = getToken();
28
30
  if (token) {
29
31
  api = new ApiClient(BASE_URL, token, handleAuthError);
32
+ authenticated = true;
30
33
  }
31
34
  /**
32
- * Get the API client, triggering browser auth if not yet authenticated.
33
- * Returns the client or throws with an auth-required message.
35
+ * Get the API client. Returns null if not authenticated.
36
+ * Does NOT auto-trigger browser auth -- use the authenticate tool instead.
34
37
  */
35
- async function getApi() {
36
- if (api)
37
- return api;
38
- const newToken = await authenticateViaBrowser(BASE_URL);
39
- api = new ApiClient(BASE_URL, newToken, handleAuthError);
38
+ function getApi() {
40
39
  return api;
41
40
  }
41
+ /**
42
+ * Returns an auth-required response for tools when not authenticated.
43
+ */
44
+ function authRequiredResponse() {
45
+ return {
46
+ content: [{
47
+ type: "text",
48
+ text: "Not authenticated with Systematics. Please run the 'authenticate' tool first to connect your account.",
49
+ }],
50
+ isError: true,
51
+ };
52
+ }
42
53
  const server = new McpServer({
43
- name: "dovito",
44
- version: "1.0.0",
54
+ name: "systematics",
55
+ version: "1.0.2",
56
+ });
57
+ // ── Authentication ────────────────────────────────────────────────────────
58
+ server.tool("authenticate", `Sign in to Systematics via your browser. ${authenticated ? "(Currently authenticated)" : "(Not authenticated -- run this first)"}`, {}, async () => {
59
+ if (authenticated && api) {
60
+ return { content: [{ type: "text", text: "Already authenticated with Systematics. To re-authenticate, run 'reauthenticate'." }] };
61
+ }
62
+ try {
63
+ const newToken = await authenticateViaBrowser(BASE_URL);
64
+ api = new ApiClient(BASE_URL, newToken, handleAuthError);
65
+ authenticated = true;
66
+ return { content: [{ type: "text", text: "Successfully authenticated with Systematics. You can now use all other tools." }] };
67
+ }
68
+ catch (err) {
69
+ return { content: [{ type: "text", text: `Authentication failed: ${err instanceof Error ? err.message : "Unknown error"}` }], isError: true };
70
+ }
71
+ });
72
+ server.tool("reauthenticate", "Clear stored credentials and sign in again with a new token", {}, async () => {
73
+ clearStoredToken();
74
+ api = null;
75
+ authenticated = false;
76
+ try {
77
+ const newToken = await authenticateViaBrowser(BASE_URL);
78
+ api = new ApiClient(BASE_URL, newToken, handleAuthError);
79
+ authenticated = true;
80
+ return { content: [{ type: "text", text: "Re-authenticated successfully with a new token." }] };
81
+ }
82
+ catch (err) {
83
+ return { content: [{ type: "text", text: `Authentication failed: ${err instanceof Error ? err.message : "Unknown error"}` }], isError: true };
84
+ }
45
85
  });
46
86
  // ── Businesses ─────────────────────────────────────────────────────────────
47
87
  server.tool("list_businesses", "List all businesses (clients) in the Dovito portal", {}, async () => {
48
- const data = await (await getApi()).get("/api/businesses");
88
+ const client = getApi();
89
+ if (!client)
90
+ return authRequiredResponse();
91
+ const data = await client.get("/api/businesses");
49
92
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
50
93
  });
51
94
  server.tool("get_business", "Get a single business by ID", { businessId: z.string().describe("Business UUID") }, async ({ businessId }) => {
52
- const data = await (await getApi()).get(`/api/businesses/${businessId}`);
95
+ const client = getApi();
96
+ if (!client)
97
+ return authRequiredResponse();
98
+ const data = await client.get(`/api/businesses/${businessId}`);
53
99
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
54
100
  });
55
101
  server.tool("create_business", "Create a new business (client company)", {
@@ -62,7 +108,10 @@ server.tool("create_business", "Create a new business (client company)", {
62
108
  status: z.enum(["pending", "approved", "active"]).optional(),
63
109
  ownerUserId: z.string().optional().describe("User ID of the business owner, or 'none'"),
64
110
  }, async (params) => {
65
- const data = await (await getApi()).post("/api/businesses", params);
111
+ const client = getApi();
112
+ if (!client)
113
+ return authRequiredResponse();
114
+ const data = await client.post("/api/businesses", params);
66
115
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
67
116
  });
68
117
  server.tool("update_business", "Update an existing business", {
@@ -77,16 +126,25 @@ server.tool("update_business", "Update an existing business", {
77
126
  status: z.enum(["pending", "approved", "active"]).optional(),
78
127
  assignedAmId: z.string().nullable().optional().describe("Account manager user ID"),
79
128
  }, async ({ businessId, ...updates }) => {
80
- const data = await (await getApi()).patch(`/api/businesses/${businessId}`, updates);
129
+ const client = getApi();
130
+ if (!client)
131
+ return authRequiredResponse();
132
+ const data = await client.patch(`/api/businesses/${businessId}`, updates);
81
133
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
82
134
  });
83
135
  // ── Projects ───────────────────────────────────────────────────────────────
84
136
  server.tool("list_projects", "List all projects across businesses", {}, async () => {
85
- const data = await (await getApi()).get("/api/projects");
137
+ const client = getApi();
138
+ if (!client)
139
+ return authRequiredResponse();
140
+ const data = await client.get("/api/projects");
86
141
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
87
142
  });
88
143
  server.tool("get_project", "Get a single project by ID", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
89
- const data = await (await getApi()).get(`/api/projects/${projectId}`);
144
+ const client = getApi();
145
+ if (!client)
146
+ return authRequiredResponse();
147
+ const data = await client.get(`/api/projects/${projectId}`);
90
148
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
91
149
  });
92
150
  server.tool("create_project", "Create a new project under a business", {
@@ -100,7 +158,10 @@ server.tool("create_project", "Create a new project under a business", {
100
158
  startDate: z.string().optional().describe("ISO date"),
101
159
  estimatedEndDate: z.string().optional().describe("ISO date"),
102
160
  }, async (params) => {
103
- const data = await (await getApi()).post("/api/projects", params);
161
+ const client = getApi();
162
+ if (!client)
163
+ return authRequiredResponse();
164
+ const data = await client.post("/api/projects", params);
104
165
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
105
166
  });
106
167
  server.tool("update_project", "Update an existing project", {
@@ -113,7 +174,10 @@ server.tool("update_project", "Update an existing project", {
113
174
  startDate: z.string().optional().describe("ISO date"),
114
175
  estimatedEndDate: z.string().optional().describe("ISO date"),
115
176
  }, async ({ projectId, ...updates }) => {
116
- const data = await (await getApi()).patch(`/api/projects/${projectId}`, updates);
177
+ const client = getApi();
178
+ if (!client)
179
+ return authRequiredResponse();
180
+ const data = await client.patch(`/api/projects/${projectId}`, updates);
117
181
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
118
182
  });
119
183
  // ── Tasks ──────────────────────────────────────────────────────────────────
@@ -124,11 +188,17 @@ server.tool("list_tasks", "List all tasks, optionally filtered by business", {
124
188
  if (businessId)
125
189
  params.set("businessId", businessId);
126
190
  const query = params.toString() ? `?${params}` : "";
127
- const data = await (await getApi()).get(`/api/tasks${query}`);
191
+ const client = getApi();
192
+ if (!client)
193
+ return authRequiredResponse();
194
+ const data = await client.get(`/api/tasks${query}`);
128
195
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
129
196
  });
130
197
  server.tool("list_project_tasks", "List tasks for a specific project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
131
- const data = await (await getApi()).get(`/api/projects/${projectId}/tasks`);
198
+ const client = getApi();
199
+ if (!client)
200
+ return authRequiredResponse();
201
+ const data = await client.get(`/api/projects/${projectId}/tasks`);
132
202
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
133
203
  });
134
204
  server.tool("create_task", "Create a task under a project", {
@@ -140,7 +210,10 @@ server.tool("create_task", "Create a task under a project", {
140
210
  startDate: z.string().optional().describe("ISO date"),
141
211
  dueDate: z.string().optional().describe("ISO date"),
142
212
  }, async ({ projectId, ...body }) => {
143
- const data = await (await getApi()).post(`/api/projects/${projectId}/tasks`, body);
213
+ const client = getApi();
214
+ if (!client)
215
+ return authRequiredResponse();
216
+ const data = await client.post(`/api/projects/${projectId}/tasks`, body);
144
217
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
145
218
  });
146
219
  server.tool("update_task", "Update an existing task", {
@@ -154,7 +227,10 @@ server.tool("update_task", "Update an existing task", {
154
227
  startDate: z.string().optional().describe("ISO date"),
155
228
  dueDate: z.string().optional().describe("ISO date"),
156
229
  }, async ({ projectId, taskId, ...updates }) => {
157
- const data = await (await getApi()).patch(`/api/projects/${projectId}/tasks/${taskId}`, updates);
230
+ const client = getApi();
231
+ if (!client)
232
+ return authRequiredResponse();
233
+ const data = await client.patch(`/api/projects/${projectId}/tasks/${taskId}`, updates);
158
234
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
159
235
  });
160
236
  server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
@@ -162,13 +238,19 @@ server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
162
238
  taskId: z.string().describe("Task UUID"),
163
239
  confirm: z.literal(true).describe("Must be true to confirm deletion"),
164
240
  }, async ({ projectId, taskId }) => {
165
- await (await getApi()).delete(`/api/projects/${projectId}/tasks/${taskId}`);
241
+ const client = getApi();
242
+ if (!client)
243
+ return authRequiredResponse();
244
+ await client.delete(`/api/projects/${projectId}/tasks/${taskId}`);
166
245
  return { content: [{ type: "text", text: "Task deleted." }] };
167
246
  });
168
247
  // ── Catalog ────────────────────────────────────────────────────────────────
169
248
  server.tool("list_catalog_items", "List all items in the line item catalog", { all: z.boolean().optional().describe("Include inactive items") }, async ({ all }) => {
170
249
  const query = all ? "?all=true" : "";
171
- const data = await (await getApi()).get(`/api/catalog${query}`);
250
+ const client = getApi();
251
+ if (!client)
252
+ return authRequiredResponse();
253
+ const data = await client.get(`/api/catalog${query}`);
172
254
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
173
255
  });
174
256
  server.tool("create_catalog_item", "Add an item to the line item catalog", {
@@ -179,7 +261,10 @@ server.tool("create_catalog_item", "Add an item to the line item catalog", {
179
261
  unitAmount: z.number().int().min(0).describe("Price in cents"),
180
262
  unit: z.string().optional().describe("hour, each, month, project, per seat, quarterly, annual"),
181
263
  }, async (params) => {
182
- const data = await (await getApi()).post("/api/catalog", params);
264
+ const client = getApi();
265
+ if (!client)
266
+ return authRequiredResponse();
267
+ const data = await client.post("/api/catalog", params);
183
268
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
184
269
  });
185
270
  server.tool("update_catalog_item", "Update a catalog item", {
@@ -192,7 +277,10 @@ server.tool("update_catalog_item", "Update a catalog item", {
192
277
  unit: z.string().optional(),
193
278
  isActive: z.boolean().optional(),
194
279
  }, async ({ itemId, ...updates }) => {
195
- const data = await (await getApi()).patch(`/api/catalog/${itemId}`, updates);
280
+ const client = getApi();
281
+ if (!client)
282
+ return authRequiredResponse();
283
+ const data = await client.patch(`/api/catalog/${itemId}`, updates);
196
284
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
197
285
  });
198
286
  // ── Users ──────────────────────────────────────────────────────────────────
@@ -206,12 +294,18 @@ server.tool("list_users", "List all users in the portal", {
206
294
  if (includeStaff)
207
295
  params.set("includeStaff", "true");
208
296
  const query = params.toString() ? `?${params}` : "";
209
- const data = await (await getApi()).get(`/api/admin/users${query}`);
297
+ const client = getApi();
298
+ if (!client)
299
+ return authRequiredResponse();
300
+ const data = await client.get(`/api/admin/users${query}`);
210
301
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
211
302
  });
212
303
  // ── Proposals ──────────────────────────────────────────────────────────────
213
304
  server.tool("list_proposals", "List proposals for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
214
- const data = await (await getApi()).get(`/api/projects/${projectId}/proposals`);
305
+ const client = getApi();
306
+ if (!client)
307
+ return authRequiredResponse();
308
+ const data = await client.get(`/api/projects/${projectId}/proposals`);
215
309
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
216
310
  });
217
311
  server.tool("create_proposal", "Create a proposal (quote) for a project", {
@@ -225,12 +319,18 @@ server.tool("create_proposal", "Create a proposal (quote) for a project", {
225
319
  })).describe("Line items for the proposal"),
226
320
  expiresAt: z.string().optional().describe("ISO date for expiration"),
227
321
  }, async ({ projectId, ...body }) => {
228
- const data = await (await getApi()).post(`/api/projects/${projectId}/proposals`, body);
322
+ const client = getApi();
323
+ if (!client)
324
+ return authRequiredResponse();
325
+ const data = await client.post(`/api/projects/${projectId}/proposals`, body);
229
326
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
230
327
  });
231
328
  // ── Conversations & Messages ───────────────────────────────────────────────
232
329
  server.tool("list_conversations", "List all conversations (message threads) for the service account", {}, async () => {
233
- const data = await (await getApi()).get("/api/messages");
330
+ const client = getApi();
331
+ if (!client)
332
+ return authRequiredResponse();
333
+ const data = await client.get("/api/messages");
234
334
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
235
335
  });
236
336
  server.tool("get_conversation_messages", "Get messages in a conversation thread", {
@@ -241,7 +341,10 @@ server.tool("get_conversation_messages", "Get messages in a conversation thread"
241
341
  if (cursor)
242
342
  params.set("cursor", cursor);
243
343
  const query = params.toString() ? `?${params}` : "";
244
- const data = await (await getApi()).get(`/api/messages/${conversationId}${query}`);
344
+ const client = getApi();
345
+ if (!client)
346
+ return authRequiredResponse();
347
+ const data = await client.get(`/api/messages/${conversationId}${query}`);
245
348
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
246
349
  });
247
350
  server.tool("create_conversation", "Create a new conversation (DM, group, project, or business thread)", {
@@ -251,7 +354,10 @@ server.tool("create_conversation", "Create a new conversation (DM, group, projec
251
354
  projectId: z.string().optional().describe("Project UUID (for project type)"),
252
355
  businessId: z.string().optional().describe("Business UUID (for business type)"),
253
356
  }, async (params) => {
254
- const data = await (await getApi()).post("/api/conversations", params);
357
+ const client = getApi();
358
+ if (!client)
359
+ return authRequiredResponse();
360
+ const data = await client.post("/api/conversations", params);
255
361
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
256
362
  });
257
363
  server.tool("send_message", "Send a message in a conversation", {
@@ -259,12 +365,18 @@ server.tool("send_message", "Send a message in a conversation", {
259
365
  content: z.string().describe("Message content (1-10000 chars)"),
260
366
  parentMessageId: z.string().optional().describe("Reply to a specific message"),
261
367
  }, async ({ conversationId, ...body }) => {
262
- const data = await (await getApi()).post(`/api/messages/${conversationId}`, body);
368
+ const client = getApi();
369
+ if (!client)
370
+ return authRequiredResponse();
371
+ const data = await client.post(`/api/messages/${conversationId}`, body);
263
372
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
264
373
  });
265
374
  // ── Deliverables ───────────────────────────────────────────────────────────
266
375
  server.tool("list_deliverables", "List deliverables for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
267
- const data = await (await getApi()).get(`/api/projects/${projectId}/deliverables`);
376
+ const client = getApi();
377
+ if (!client)
378
+ return authRequiredResponse();
379
+ const data = await client.get(`/api/projects/${projectId}/deliverables`);
268
380
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
269
381
  });
270
382
  server.tool("create_deliverable", "Create a deliverable under a project", {
@@ -273,7 +385,10 @@ server.tool("create_deliverable", "Create a deliverable under a project", {
273
385
  description: z.string().optional(),
274
386
  status: z.enum(["pending", "in_progress", "completed"]).optional(),
275
387
  }, async ({ projectId, ...body }) => {
276
- const data = await (await getApi()).post(`/api/projects/${projectId}/deliverables`, body);
388
+ const client = getApi();
389
+ if (!client)
390
+ return authRequiredResponse();
391
+ const data = await client.post(`/api/projects/${projectId}/deliverables`, body);
277
392
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
278
393
  });
279
394
  server.tool("update_deliverable", "Update a deliverable", {
@@ -283,12 +398,18 @@ server.tool("update_deliverable", "Update a deliverable", {
283
398
  description: z.string().optional(),
284
399
  status: z.enum(["pending", "in_progress", "completed"]).optional(),
285
400
  }, async ({ projectId, deliverableId, ...updates }) => {
286
- const data = await (await getApi()).patch(`/api/projects/${projectId}/deliverables/${deliverableId}`, updates);
401
+ const client = getApi();
402
+ if (!client)
403
+ return authRequiredResponse();
404
+ const data = await client.patch(`/api/projects/${projectId}/deliverables/${deliverableId}`, updates);
287
405
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
288
406
  });
289
407
  // ── Applications (Pipeline) ───────────────────────────────────────────────
290
408
  server.tool("list_applications", "List all applications in the pipeline", {}, async () => {
291
- const data = await (await getApi()).get("/api/admin/applications");
409
+ const client = getApi();
410
+ if (!client)
411
+ return authRequiredResponse();
412
+ const data = await client.get("/api/admin/applications");
292
413
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
293
414
  });
294
415
  // ── Start server ───────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systematics-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Claude Code MCP server for the Systematics platform by Dovito Business Solutions",
6
6
  "bin": {