systematics-mcp 1.0.0 → 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.
- 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 +53 -42
- 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);
|
|
@@ -13,32 +13,43 @@ if (!ALLOWED_HOSTS.includes(parsedUrl.hostname)) {
|
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
const BASE_URL = parsedUrl.origin;
|
|
16
|
-
// Resolve token
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
// Resolve token silently (env var or stored file only -- no browser popup)
|
|
17
|
+
function getToken() {
|
|
18
|
+
return process.env.SYSTEMATICS_TOKEN || process.env.MCP_API_KEY || getStoredToken();
|
|
19
|
+
}
|
|
20
|
+
// On 401, clear stored token so next call re-authenticates
|
|
21
|
+
function handleAuthError() {
|
|
22
|
+
clearStoredToken();
|
|
23
|
+
api = null;
|
|
24
|
+
}
|
|
25
|
+
// Lazy API client -- initialized on first use or after auth
|
|
26
|
+
let api = null;
|
|
27
|
+
const token = getToken();
|
|
28
|
+
if (token) {
|
|
29
|
+
api = new ApiClient(BASE_URL, token, handleAuthError);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the API client, triggering browser auth if not yet authenticated.
|
|
33
|
+
* Returns the client or throws with an auth-required message.
|
|
34
|
+
*/
|
|
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);
|
|
40
|
+
return api;
|
|
28
41
|
}
|
|
29
|
-
const token = await resolveToken();
|
|
30
|
-
const api = new ApiClient(BASE_URL, token);
|
|
31
42
|
const server = new McpServer({
|
|
32
43
|
name: "dovito",
|
|
33
44
|
version: "1.0.0",
|
|
34
45
|
});
|
|
35
46
|
// ── Businesses ─────────────────────────────────────────────────────────────
|
|
36
47
|
server.tool("list_businesses", "List all businesses (clients) in the Dovito portal", {}, async () => {
|
|
37
|
-
const data = await
|
|
48
|
+
const data = await (await getApi()).get("/api/businesses");
|
|
38
49
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
39
50
|
});
|
|
40
51
|
server.tool("get_business", "Get a single business by ID", { businessId: z.string().describe("Business UUID") }, async ({ businessId }) => {
|
|
41
|
-
const data = await
|
|
52
|
+
const data = await (await getApi()).get(`/api/businesses/${businessId}`);
|
|
42
53
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
43
54
|
});
|
|
44
55
|
server.tool("create_business", "Create a new business (client company)", {
|
|
@@ -51,7 +62,7 @@ server.tool("create_business", "Create a new business (client company)", {
|
|
|
51
62
|
status: z.enum(["pending", "approved", "active"]).optional(),
|
|
52
63
|
ownerUserId: z.string().optional().describe("User ID of the business owner, or 'none'"),
|
|
53
64
|
}, async (params) => {
|
|
54
|
-
const data = await
|
|
65
|
+
const data = await (await getApi()).post("/api/businesses", params);
|
|
55
66
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
56
67
|
});
|
|
57
68
|
server.tool("update_business", "Update an existing business", {
|
|
@@ -66,16 +77,16 @@ server.tool("update_business", "Update an existing business", {
|
|
|
66
77
|
status: z.enum(["pending", "approved", "active"]).optional(),
|
|
67
78
|
assignedAmId: z.string().nullable().optional().describe("Account manager user ID"),
|
|
68
79
|
}, async ({ businessId, ...updates }) => {
|
|
69
|
-
const data = await
|
|
80
|
+
const data = await (await getApi()).patch(`/api/businesses/${businessId}`, updates);
|
|
70
81
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
71
82
|
});
|
|
72
83
|
// ── Projects ───────────────────────────────────────────────────────────────
|
|
73
84
|
server.tool("list_projects", "List all projects across businesses", {}, async () => {
|
|
74
|
-
const data = await
|
|
85
|
+
const data = await (await getApi()).get("/api/projects");
|
|
75
86
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
76
87
|
});
|
|
77
88
|
server.tool("get_project", "Get a single project by ID", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
78
|
-
const data = await
|
|
89
|
+
const data = await (await getApi()).get(`/api/projects/${projectId}`);
|
|
79
90
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
80
91
|
});
|
|
81
92
|
server.tool("create_project", "Create a new project under a business", {
|
|
@@ -89,7 +100,7 @@ server.tool("create_project", "Create a new project under a business", {
|
|
|
89
100
|
startDate: z.string().optional().describe("ISO date"),
|
|
90
101
|
estimatedEndDate: z.string().optional().describe("ISO date"),
|
|
91
102
|
}, async (params) => {
|
|
92
|
-
const data = await
|
|
103
|
+
const data = await (await getApi()).post("/api/projects", params);
|
|
93
104
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
94
105
|
});
|
|
95
106
|
server.tool("update_project", "Update an existing project", {
|
|
@@ -102,7 +113,7 @@ server.tool("update_project", "Update an existing project", {
|
|
|
102
113
|
startDate: z.string().optional().describe("ISO date"),
|
|
103
114
|
estimatedEndDate: z.string().optional().describe("ISO date"),
|
|
104
115
|
}, async ({ projectId, ...updates }) => {
|
|
105
|
-
const data = await
|
|
116
|
+
const data = await (await getApi()).patch(`/api/projects/${projectId}`, updates);
|
|
106
117
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
107
118
|
});
|
|
108
119
|
// ── Tasks ──────────────────────────────────────────────────────────────────
|
|
@@ -113,11 +124,11 @@ server.tool("list_tasks", "List all tasks, optionally filtered by business", {
|
|
|
113
124
|
if (businessId)
|
|
114
125
|
params.set("businessId", businessId);
|
|
115
126
|
const query = params.toString() ? `?${params}` : "";
|
|
116
|
-
const data = await
|
|
127
|
+
const data = await (await getApi()).get(`/api/tasks${query}`);
|
|
117
128
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
118
129
|
});
|
|
119
130
|
server.tool("list_project_tasks", "List tasks for a specific project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
120
|
-
const data = await
|
|
131
|
+
const data = await (await getApi()).get(`/api/projects/${projectId}/tasks`);
|
|
121
132
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
122
133
|
});
|
|
123
134
|
server.tool("create_task", "Create a task under a project", {
|
|
@@ -129,7 +140,7 @@ server.tool("create_task", "Create a task under a project", {
|
|
|
129
140
|
startDate: z.string().optional().describe("ISO date"),
|
|
130
141
|
dueDate: z.string().optional().describe("ISO date"),
|
|
131
142
|
}, async ({ projectId, ...body }) => {
|
|
132
|
-
const data = await
|
|
143
|
+
const data = await (await getApi()).post(`/api/projects/${projectId}/tasks`, body);
|
|
133
144
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
134
145
|
});
|
|
135
146
|
server.tool("update_task", "Update an existing task", {
|
|
@@ -143,7 +154,7 @@ server.tool("update_task", "Update an existing task", {
|
|
|
143
154
|
startDate: z.string().optional().describe("ISO date"),
|
|
144
155
|
dueDate: z.string().optional().describe("ISO date"),
|
|
145
156
|
}, async ({ projectId, taskId, ...updates }) => {
|
|
146
|
-
const data = await
|
|
157
|
+
const data = await (await getApi()).patch(`/api/projects/${projectId}/tasks/${taskId}`, updates);
|
|
147
158
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
148
159
|
});
|
|
149
160
|
server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
|
|
@@ -151,13 +162,13 @@ server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
|
|
|
151
162
|
taskId: z.string().describe("Task UUID"),
|
|
152
163
|
confirm: z.literal(true).describe("Must be true to confirm deletion"),
|
|
153
164
|
}, async ({ projectId, taskId }) => {
|
|
154
|
-
await
|
|
165
|
+
await (await getApi()).delete(`/api/projects/${projectId}/tasks/${taskId}`);
|
|
155
166
|
return { content: [{ type: "text", text: "Task deleted." }] };
|
|
156
167
|
});
|
|
157
168
|
// ── Catalog ────────────────────────────────────────────────────────────────
|
|
158
169
|
server.tool("list_catalog_items", "List all items in the line item catalog", { all: z.boolean().optional().describe("Include inactive items") }, async ({ all }) => {
|
|
159
170
|
const query = all ? "?all=true" : "";
|
|
160
|
-
const data = await
|
|
171
|
+
const data = await (await getApi()).get(`/api/catalog${query}`);
|
|
161
172
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
162
173
|
});
|
|
163
174
|
server.tool("create_catalog_item", "Add an item to the line item catalog", {
|
|
@@ -168,7 +179,7 @@ server.tool("create_catalog_item", "Add an item to the line item catalog", {
|
|
|
168
179
|
unitAmount: z.number().int().min(0).describe("Price in cents"),
|
|
169
180
|
unit: z.string().optional().describe("hour, each, month, project, per seat, quarterly, annual"),
|
|
170
181
|
}, async (params) => {
|
|
171
|
-
const data = await
|
|
182
|
+
const data = await (await getApi()).post("/api/catalog", params);
|
|
172
183
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
173
184
|
});
|
|
174
185
|
server.tool("update_catalog_item", "Update a catalog item", {
|
|
@@ -181,7 +192,7 @@ server.tool("update_catalog_item", "Update a catalog item", {
|
|
|
181
192
|
unit: z.string().optional(),
|
|
182
193
|
isActive: z.boolean().optional(),
|
|
183
194
|
}, async ({ itemId, ...updates }) => {
|
|
184
|
-
const data = await
|
|
195
|
+
const data = await (await getApi()).patch(`/api/catalog/${itemId}`, updates);
|
|
185
196
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
186
197
|
});
|
|
187
198
|
// ── Users ──────────────────────────────────────────────────────────────────
|
|
@@ -195,12 +206,12 @@ server.tool("list_users", "List all users in the portal", {
|
|
|
195
206
|
if (includeStaff)
|
|
196
207
|
params.set("includeStaff", "true");
|
|
197
208
|
const query = params.toString() ? `?${params}` : "";
|
|
198
|
-
const data = await
|
|
209
|
+
const data = await (await getApi()).get(`/api/admin/users${query}`);
|
|
199
210
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
200
211
|
});
|
|
201
212
|
// ── Proposals ──────────────────────────────────────────────────────────────
|
|
202
213
|
server.tool("list_proposals", "List proposals for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
203
|
-
const data = await
|
|
214
|
+
const data = await (await getApi()).get(`/api/projects/${projectId}/proposals`);
|
|
204
215
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
205
216
|
});
|
|
206
217
|
server.tool("create_proposal", "Create a proposal (quote) for a project", {
|
|
@@ -214,12 +225,12 @@ server.tool("create_proposal", "Create a proposal (quote) for a project", {
|
|
|
214
225
|
})).describe("Line items for the proposal"),
|
|
215
226
|
expiresAt: z.string().optional().describe("ISO date for expiration"),
|
|
216
227
|
}, async ({ projectId, ...body }) => {
|
|
217
|
-
const data = await
|
|
228
|
+
const data = await (await getApi()).post(`/api/projects/${projectId}/proposals`, body);
|
|
218
229
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
219
230
|
});
|
|
220
231
|
// ── Conversations & Messages ───────────────────────────────────────────────
|
|
221
232
|
server.tool("list_conversations", "List all conversations (message threads) for the service account", {}, async () => {
|
|
222
|
-
const data = await
|
|
233
|
+
const data = await (await getApi()).get("/api/messages");
|
|
223
234
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
224
235
|
});
|
|
225
236
|
server.tool("get_conversation_messages", "Get messages in a conversation thread", {
|
|
@@ -230,7 +241,7 @@ server.tool("get_conversation_messages", "Get messages in a conversation thread"
|
|
|
230
241
|
if (cursor)
|
|
231
242
|
params.set("cursor", cursor);
|
|
232
243
|
const query = params.toString() ? `?${params}` : "";
|
|
233
|
-
const data = await
|
|
244
|
+
const data = await (await getApi()).get(`/api/messages/${conversationId}${query}`);
|
|
234
245
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
235
246
|
});
|
|
236
247
|
server.tool("create_conversation", "Create a new conversation (DM, group, project, or business thread)", {
|
|
@@ -240,7 +251,7 @@ server.tool("create_conversation", "Create a new conversation (DM, group, projec
|
|
|
240
251
|
projectId: z.string().optional().describe("Project UUID (for project type)"),
|
|
241
252
|
businessId: z.string().optional().describe("Business UUID (for business type)"),
|
|
242
253
|
}, async (params) => {
|
|
243
|
-
const data = await
|
|
254
|
+
const data = await (await getApi()).post("/api/conversations", params);
|
|
244
255
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
245
256
|
});
|
|
246
257
|
server.tool("send_message", "Send a message in a conversation", {
|
|
@@ -248,12 +259,12 @@ server.tool("send_message", "Send a message in a conversation", {
|
|
|
248
259
|
content: z.string().describe("Message content (1-10000 chars)"),
|
|
249
260
|
parentMessageId: z.string().optional().describe("Reply to a specific message"),
|
|
250
261
|
}, async ({ conversationId, ...body }) => {
|
|
251
|
-
const data = await
|
|
262
|
+
const data = await (await getApi()).post(`/api/messages/${conversationId}`, body);
|
|
252
263
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
253
264
|
});
|
|
254
265
|
// ── Deliverables ───────────────────────────────────────────────────────────
|
|
255
266
|
server.tool("list_deliverables", "List deliverables for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
256
|
-
const data = await
|
|
267
|
+
const data = await (await getApi()).get(`/api/projects/${projectId}/deliverables`);
|
|
257
268
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
258
269
|
});
|
|
259
270
|
server.tool("create_deliverable", "Create a deliverable under a project", {
|
|
@@ -262,7 +273,7 @@ server.tool("create_deliverable", "Create a deliverable under a project", {
|
|
|
262
273
|
description: z.string().optional(),
|
|
263
274
|
status: z.enum(["pending", "in_progress", "completed"]).optional(),
|
|
264
275
|
}, async ({ projectId, ...body }) => {
|
|
265
|
-
const data = await
|
|
276
|
+
const data = await (await getApi()).post(`/api/projects/${projectId}/deliverables`, body);
|
|
266
277
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
267
278
|
});
|
|
268
279
|
server.tool("update_deliverable", "Update a deliverable", {
|
|
@@ -272,12 +283,12 @@ server.tool("update_deliverable", "Update a deliverable", {
|
|
|
272
283
|
description: z.string().optional(),
|
|
273
284
|
status: z.enum(["pending", "in_progress", "completed"]).optional(),
|
|
274
285
|
}, async ({ projectId, deliverableId, ...updates }) => {
|
|
275
|
-
const data = await
|
|
286
|
+
const data = await (await getApi()).patch(`/api/projects/${projectId}/deliverables/${deliverableId}`, updates);
|
|
276
287
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
277
288
|
});
|
|
278
289
|
// ── Applications (Pipeline) ───────────────────────────────────────────────
|
|
279
290
|
server.tool("list_applications", "List all applications in the pipeline", {}, async () => {
|
|
280
|
-
const data = await
|
|
291
|
+
const data = await (await getApi()).get("/api/admin/applications");
|
|
281
292
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
282
293
|
});
|
|
283
294
|
// ── Start server ───────────────────────────────────────────────────────────
|