systematics-mcp 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Claude Code MCP server for the [Systematics](https://app.dovito.com) platform by
6
6
 
7
7
  ### 1. Add to Claude Code
8
8
 
9
- Add this to your `~/.claude/settings.json`:
9
+ Add this to your project or global `.mcp.json` file:
10
10
 
11
11
  ```json
12
12
  {
@@ -21,12 +21,14 @@ Add this to your `~/.claude/settings.json`:
21
21
 
22
22
  ### 2. Authenticate
23
23
 
24
- Start a Claude Code session. The first time you use a Systematics tool, your browser will open to sign in. Click **Authorize Claude Code** and you're connected. Your token is saved locally at `~/.systematics/token` and reused automatically for 90 days.
24
+ Restart Claude Code, then use `/mcp` to see Systematics listed. The first time you use a Systematics tool, your browser will open to sign in. Click **Authorize Claude Code** and you're connected. Your token is saved locally at `~/.systematics/token` and reused automatically for 90 days.
25
25
 
26
26
  That's it. No API keys, no repo access, no manual setup.
27
27
 
28
28
  ## How It Works
29
29
 
30
+ - The MCP server starts silently -- no browser popup on launch
31
+ - Authentication only triggers when you actually use a Systematics tool
30
32
  - You sign in with your normal Systematics account (Google or email)
31
33
  - Claude Code gets the same permissions as your account
32
34
  - Clients see only their business data
@@ -61,9 +63,32 @@ That's it. No API keys, no repo access, no manual setup.
61
63
  | `SYSTEMATICS_TOKEN` | - | Skip browser auth by providing a token directly |
62
64
  | `DOVITO_APP_URL` | `https://app.dovito.com` | Custom app URL (for self-hosted instances) |
63
65
 
66
+ ## Development
67
+
68
+ The MCP server lives in `mcp-server/` inside the [app.dovito.com](https://github.com/dovito-dev/app.dovito.com) repository.
69
+
70
+ ```bash
71
+ cd mcp-server
72
+ npm install
73
+ npm run dev # Run locally with tsx
74
+ npm run build # Compile TypeScript
75
+ ```
76
+
77
+ ### CI/CD
78
+
79
+ Publishing to npm is automated via GitHub Actions. To release a new version:
80
+
81
+ 1. Make changes in `mcp-server/`
82
+ 2. Bump the version in `mcp-server/package.json`
83
+ 3. Push to `main`
84
+ 4. GitHub Actions builds and publishes to npm automatically
85
+
86
+ If you push without bumping the version, the workflow skips the publish step.
87
+
64
88
  ## Security
65
89
 
66
- - Tokens are hashed before storage in the database
67
- - Token file is stored with `0o600` permissions (owner-only)
90
+ - Tokens are hashed (HMAC-SHA256) before storage in the database
91
+ - Token file is stored with `0o600` permissions in a `0o700` directory
68
92
  - Auth callback uses POST (token never appears in URLs or browser history)
69
93
  - All API calls go through the same validation and rate limiting as the web UI
94
+ - SSRF prevention: `DOVITO_APP_URL` is validated against an allowlist before any network call
@@ -5,7 +5,8 @@
5
5
  export declare class ApiClient {
6
6
  private baseUrl;
7
7
  private token;
8
- constructor(baseUrl: string, token: string);
8
+ private onAuthError?;
9
+ constructor(baseUrl: string, token: string, onAuthError?: () => void);
9
10
  private request;
10
11
  get<T>(path: string): Promise<T>;
11
12
  post<T>(path: string, body: unknown): Promise<T>;
@@ -5,9 +5,11 @@
5
5
  export class ApiClient {
6
6
  baseUrl;
7
7
  token;
8
- constructor(baseUrl, token) {
8
+ onAuthError;
9
+ constructor(baseUrl, token, onAuthError) {
9
10
  this.baseUrl = baseUrl.replace(/\/$/, "");
10
11
  this.token = token;
12
+ this.onAuthError = onAuthError;
11
13
  }
12
14
  async request(method, path, body) {
13
15
  const url = `${this.baseUrl}${path}`;
@@ -21,6 +23,9 @@ export class ApiClient {
21
23
  body: body ? JSON.stringify(body) : undefined,
22
24
  });
23
25
  if (!res.ok) {
26
+ if (res.status === 401 && this.onAuthError) {
27
+ this.onAuthError();
28
+ }
24
29
  const text = await res.text();
25
30
  let msg;
26
31
  try {
@@ -29,7 +34,6 @@ export class ApiClient {
29
34
  catch {
30
35
  msg = text;
31
36
  }
32
- // Truncate error message to avoid leaking internal details
33
37
  const safeMsg = msg.length > 500 ? msg.slice(0, 500) + "..." : msg;
34
38
  throw new Error(`API ${method} ${path} returned ${res.status}: ${safeMsg}`);
35
39
  }
package/dist/auth.d.ts CHANGED
@@ -2,6 +2,10 @@
2
2
  * Get the stored personal access token, or null if not found/expired.
3
3
  */
4
4
  export declare function getStoredToken(): string | null;
5
+ /**
6
+ * Delete the stored token (e.g. on 401 so re-auth triggers next time).
7
+ */
8
+ export declare function clearStoredToken(): void;
5
9
  /**
6
10
  * Run the browser-based authentication flow:
7
11
  * 1. Start a temporary local HTTP server
package/dist/auth.js CHANGED
@@ -21,6 +21,17 @@ export function getStoredToken() {
21
21
  return null;
22
22
  }
23
23
  }
24
+ /**
25
+ * Delete the stored token (e.g. on 401 so re-auth triggers next time).
26
+ */
27
+ export function clearStoredToken() {
28
+ try {
29
+ if (existsSync(TOKEN_FILE)) {
30
+ writeFileSync(TOKEN_FILE, "", { mode: 0o600 });
31
+ }
32
+ }
33
+ catch { /* ignore */ }
34
+ }
24
35
  /**
25
36
  * Save a token to the local config directory.
26
37
  */
@@ -100,7 +111,7 @@ export async function authenticateViaBrowser(appUrl) {
100
111
  <div class="card">
101
112
  <div class="check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></div>
102
113
  <h1>Connected to Systematics</h1>
103
- <p>You can close this tab and return to Claude Code.</p>
114
+ <p>You can close this tab and return to your application.</p>
104
115
  </div>
105
116
  </body>
106
117
  </html>`);
@@ -126,7 +137,8 @@ export async function authenticateViaBrowser(appUrl) {
126
137
  return;
127
138
  }
128
139
  const callbackUrl = `http://127.0.0.1:${addr.port}/callback`;
129
- const authUrl = `${appUrl}/auth/mcp?callback=${encodeURIComponent(callbackUrl)}`;
140
+ const clientName = process.env.MCP_CLIENT_NAME || "Claude Code";
141
+ const authUrl = `${appUrl}/auth/mcp?callback=${encodeURIComponent(callbackUrl)}&client=${encodeURIComponent(clientName)}`;
130
142
  // Write to stderr so Claude Code can show it (stdout is MCP protocol)
131
143
  process.stderr.write(`\nOpening browser to authenticate with Systematics...\n`);
132
144
  process.stderr.write(`If the browser doesn't open, visit: ${authUrl}\n\n`);
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { ApiClient } from "./api-client.js";
6
- import { getStoredToken, authenticateViaBrowser } from "./auth.js";
6
+ import { getStoredToken, authenticateViaBrowser, clearStoredToken } from "./auth.js";
7
7
  // Validate DOVITO_APP_URL against allowed hostnames to prevent SSRF
8
8
  const rawUrl = process.env.DOVITO_APP_URL || "https://app.dovito.com";
9
9
  const parsedUrl = new URL(rawUrl);
@@ -17,34 +17,85 @@ const BASE_URL = parsedUrl.origin;
17
17
  function getToken() {
18
18
  return process.env.SYSTEMATICS_TOKEN || process.env.MCP_API_KEY || getStoredToken();
19
19
  }
20
- // Lazy API client -- initialized on first use or after auth
20
+ // On 401, clear stored token so next call re-authenticates
21
+ function handleAuthError() {
22
+ clearStoredToken();
23
+ api = null;
24
+ authenticated = false;
25
+ }
26
+ // Track auth state
27
+ let authenticated = false;
21
28
  let api = null;
22
29
  const token = getToken();
23
30
  if (token) {
24
- api = new ApiClient(BASE_URL, token);
31
+ api = new ApiClient(BASE_URL, token, handleAuthError);
32
+ authenticated = true;
25
33
  }
26
34
  /**
27
- * Get the API client, triggering browser auth if not yet authenticated.
28
- * 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.
29
37
  */
30
- async function getApi() {
31
- if (api)
32
- return api;
33
- const newToken = await authenticateViaBrowser(BASE_URL);
34
- api = new ApiClient(BASE_URL, newToken);
38
+ function getApi() {
35
39
  return api;
36
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
+ }
37
53
  const server = new McpServer({
38
- name: "dovito",
39
- 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
+ }
40
85
  });
41
86
  // ── Businesses ─────────────────────────────────────────────────────────────
42
87
  server.tool("list_businesses", "List all businesses (clients) in the Dovito portal", {}, async () => {
43
- 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");
44
92
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
45
93
  });
46
94
  server.tool("get_business", "Get a single business by ID", { businessId: z.string().describe("Business UUID") }, async ({ businessId }) => {
47
- 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}`);
48
99
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
49
100
  });
50
101
  server.tool("create_business", "Create a new business (client company)", {
@@ -57,7 +108,10 @@ server.tool("create_business", "Create a new business (client company)", {
57
108
  status: z.enum(["pending", "approved", "active"]).optional(),
58
109
  ownerUserId: z.string().optional().describe("User ID of the business owner, or 'none'"),
59
110
  }, async (params) => {
60
- 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);
61
115
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
62
116
  });
63
117
  server.tool("update_business", "Update an existing business", {
@@ -72,16 +126,25 @@ server.tool("update_business", "Update an existing business", {
72
126
  status: z.enum(["pending", "approved", "active"]).optional(),
73
127
  assignedAmId: z.string().nullable().optional().describe("Account manager user ID"),
74
128
  }, async ({ businessId, ...updates }) => {
75
- 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);
76
133
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
77
134
  });
78
135
  // ── Projects ───────────────────────────────────────────────────────────────
79
136
  server.tool("list_projects", "List all projects across businesses", {}, async () => {
80
- 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");
81
141
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
82
142
  });
83
143
  server.tool("get_project", "Get a single project by ID", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
84
- 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}`);
85
148
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
86
149
  });
87
150
  server.tool("create_project", "Create a new project under a business", {
@@ -95,7 +158,10 @@ server.tool("create_project", "Create a new project under a business", {
95
158
  startDate: z.string().optional().describe("ISO date"),
96
159
  estimatedEndDate: z.string().optional().describe("ISO date"),
97
160
  }, async (params) => {
98
- 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);
99
165
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
100
166
  });
101
167
  server.tool("update_project", "Update an existing project", {
@@ -108,7 +174,10 @@ server.tool("update_project", "Update an existing project", {
108
174
  startDate: z.string().optional().describe("ISO date"),
109
175
  estimatedEndDate: z.string().optional().describe("ISO date"),
110
176
  }, async ({ projectId, ...updates }) => {
111
- 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);
112
181
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
113
182
  });
114
183
  // ── Tasks ──────────────────────────────────────────────────────────────────
@@ -119,11 +188,17 @@ server.tool("list_tasks", "List all tasks, optionally filtered by business", {
119
188
  if (businessId)
120
189
  params.set("businessId", businessId);
121
190
  const query = params.toString() ? `?${params}` : "";
122
- 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}`);
123
195
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
124
196
  });
125
197
  server.tool("list_project_tasks", "List tasks for a specific project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
126
- 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`);
127
202
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
128
203
  });
129
204
  server.tool("create_task", "Create a task under a project", {
@@ -135,7 +210,10 @@ server.tool("create_task", "Create a task under a project", {
135
210
  startDate: z.string().optional().describe("ISO date"),
136
211
  dueDate: z.string().optional().describe("ISO date"),
137
212
  }, async ({ projectId, ...body }) => {
138
- 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);
139
217
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
140
218
  });
141
219
  server.tool("update_task", "Update an existing task", {
@@ -149,7 +227,10 @@ server.tool("update_task", "Update an existing task", {
149
227
  startDate: z.string().optional().describe("ISO date"),
150
228
  dueDate: z.string().optional().describe("ISO date"),
151
229
  }, async ({ projectId, taskId, ...updates }) => {
152
- 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);
153
234
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
154
235
  });
155
236
  server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
@@ -157,13 +238,19 @@ server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
157
238
  taskId: z.string().describe("Task UUID"),
158
239
  confirm: z.literal(true).describe("Must be true to confirm deletion"),
159
240
  }, async ({ projectId, taskId }) => {
160
- 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}`);
161
245
  return { content: [{ type: "text", text: "Task deleted." }] };
162
246
  });
163
247
  // ── Catalog ────────────────────────────────────────────────────────────────
164
248
  server.tool("list_catalog_items", "List all items in the line item catalog", { all: z.boolean().optional().describe("Include inactive items") }, async ({ all }) => {
165
249
  const query = all ? "?all=true" : "";
166
- 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}`);
167
254
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
168
255
  });
169
256
  server.tool("create_catalog_item", "Add an item to the line item catalog", {
@@ -174,7 +261,10 @@ server.tool("create_catalog_item", "Add an item to the line item catalog", {
174
261
  unitAmount: z.number().int().min(0).describe("Price in cents"),
175
262
  unit: z.string().optional().describe("hour, each, month, project, per seat, quarterly, annual"),
176
263
  }, async (params) => {
177
- 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);
178
268
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
179
269
  });
180
270
  server.tool("update_catalog_item", "Update a catalog item", {
@@ -187,7 +277,10 @@ server.tool("update_catalog_item", "Update a catalog item", {
187
277
  unit: z.string().optional(),
188
278
  isActive: z.boolean().optional(),
189
279
  }, async ({ itemId, ...updates }) => {
190
- 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);
191
284
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
192
285
  });
193
286
  // ── Users ──────────────────────────────────────────────────────────────────
@@ -201,12 +294,18 @@ server.tool("list_users", "List all users in the portal", {
201
294
  if (includeStaff)
202
295
  params.set("includeStaff", "true");
203
296
  const query = params.toString() ? `?${params}` : "";
204
- 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}`);
205
301
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
206
302
  });
207
303
  // ── Proposals ──────────────────────────────────────────────────────────────
208
304
  server.tool("list_proposals", "List proposals for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
209
- 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`);
210
309
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
211
310
  });
212
311
  server.tool("create_proposal", "Create a proposal (quote) for a project", {
@@ -220,12 +319,18 @@ server.tool("create_proposal", "Create a proposal (quote) for a project", {
220
319
  })).describe("Line items for the proposal"),
221
320
  expiresAt: z.string().optional().describe("ISO date for expiration"),
222
321
  }, async ({ projectId, ...body }) => {
223
- 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);
224
326
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
225
327
  });
226
328
  // ── Conversations & Messages ───────────────────────────────────────────────
227
329
  server.tool("list_conversations", "List all conversations (message threads) for the service account", {}, async () => {
228
- 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");
229
334
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
230
335
  });
231
336
  server.tool("get_conversation_messages", "Get messages in a conversation thread", {
@@ -236,7 +341,10 @@ server.tool("get_conversation_messages", "Get messages in a conversation thread"
236
341
  if (cursor)
237
342
  params.set("cursor", cursor);
238
343
  const query = params.toString() ? `?${params}` : "";
239
- 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}`);
240
348
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
241
349
  });
242
350
  server.tool("create_conversation", "Create a new conversation (DM, group, project, or business thread)", {
@@ -246,7 +354,10 @@ server.tool("create_conversation", "Create a new conversation (DM, group, projec
246
354
  projectId: z.string().optional().describe("Project UUID (for project type)"),
247
355
  businessId: z.string().optional().describe("Business UUID (for business type)"),
248
356
  }, async (params) => {
249
- 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);
250
361
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
251
362
  });
252
363
  server.tool("send_message", "Send a message in a conversation", {
@@ -254,12 +365,18 @@ server.tool("send_message", "Send a message in a conversation", {
254
365
  content: z.string().describe("Message content (1-10000 chars)"),
255
366
  parentMessageId: z.string().optional().describe("Reply to a specific message"),
256
367
  }, async ({ conversationId, ...body }) => {
257
- 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);
258
372
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
259
373
  });
260
374
  // ── Deliverables ───────────────────────────────────────────────────────────
261
375
  server.tool("list_deliverables", "List deliverables for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
262
- 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`);
263
380
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
264
381
  });
265
382
  server.tool("create_deliverable", "Create a deliverable under a project", {
@@ -268,7 +385,10 @@ server.tool("create_deliverable", "Create a deliverable under a project", {
268
385
  description: z.string().optional(),
269
386
  status: z.enum(["pending", "in_progress", "completed"]).optional(),
270
387
  }, async ({ projectId, ...body }) => {
271
- 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);
272
392
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
273
393
  });
274
394
  server.tool("update_deliverable", "Update a deliverable", {
@@ -278,12 +398,18 @@ server.tool("update_deliverable", "Update a deliverable", {
278
398
  description: z.string().optional(),
279
399
  status: z.enum(["pending", "in_progress", "completed"]).optional(),
280
400
  }, async ({ projectId, deliverableId, ...updates }) => {
281
- 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);
282
405
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
283
406
  });
284
407
  // ── Applications (Pipeline) ───────────────────────────────────────────────
285
408
  server.tool("list_applications", "List all applications in the pipeline", {}, async () => {
286
- 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");
287
413
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
288
414
  });
289
415
  // ── Start server ───────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systematics-mcp",
3
- "version": "1.0.1",
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": {