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 +29 -4
- package/dist/api-client.d.ts +2 -1
- package/dist/api-client.js +6 -2
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +14 -2
- package/dist/index.js +165 -39
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/api-client.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
export declare class ApiClient {
|
|
6
6
|
private baseUrl;
|
|
7
7
|
private token;
|
|
8
|
-
|
|
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>;
|
package/dist/api-client.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
export class ApiClient {
|
|
6
6
|
baseUrl;
|
|
7
7
|
token;
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
28
|
-
*
|
|
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
|
-
|
|
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: "
|
|
39
|
-
version: "1.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ───────────────────────────────────────────────────────────
|