systematics-mcp 1.0.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 +69 -0
- package/dist/api-client.d.ts +14 -0
- package/dist/api-client.js +53 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +141 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +291 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# systematics-mcp
|
|
2
|
+
|
|
3
|
+
Claude Code MCP server for the [Systematics](https://app.dovito.com) platform by Dovito Business Solutions. Connects Claude Code to your Systematics account for managing businesses, projects, tasks, proposals, conversations, and more through natural language.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Add to Claude Code
|
|
8
|
+
|
|
9
|
+
Add this to your `~/.claude/settings.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"systematics": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "systematics-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Authenticate
|
|
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.
|
|
25
|
+
|
|
26
|
+
That's it. No API keys, no repo access, no manual setup.
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
- You sign in with your normal Systematics account (Google or email)
|
|
31
|
+
- Claude Code gets the same permissions as your account
|
|
32
|
+
- Clients see only their business data
|
|
33
|
+
- Staff see what their role allows
|
|
34
|
+
- Admins get full access
|
|
35
|
+
|
|
36
|
+
## Available Tools
|
|
37
|
+
|
|
38
|
+
| Category | Tools |
|
|
39
|
+
|----------|-------|
|
|
40
|
+
| Businesses | list, get, create, update |
|
|
41
|
+
| Projects | list, get, create, update |
|
|
42
|
+
| Tasks | list, list by project, create, update, delete |
|
|
43
|
+
| Catalog | list, create, update |
|
|
44
|
+
| Users | list (with filters) |
|
|
45
|
+
| Proposals | list, create |
|
|
46
|
+
| Conversations | list, read messages, create, send message |
|
|
47
|
+
| Deliverables | list, create, update |
|
|
48
|
+
| Pipeline | list applications |
|
|
49
|
+
|
|
50
|
+
## Token Management
|
|
51
|
+
|
|
52
|
+
- View and revoke tokens at **Settings > API Tokens** in the web app
|
|
53
|
+
- Tokens expire after 90 days
|
|
54
|
+
- Maximum 10 tokens per user
|
|
55
|
+
- To re-authenticate, delete `~/.systematics/token` and restart Claude Code
|
|
56
|
+
|
|
57
|
+
## Environment Variables (Optional)
|
|
58
|
+
|
|
59
|
+
| Variable | Default | Description |
|
|
60
|
+
|----------|---------|-------------|
|
|
61
|
+
| `SYSTEMATICS_TOKEN` | - | Skip browser auth by providing a token directly |
|
|
62
|
+
| `DOVITO_APP_URL` | `https://app.dovito.com` | Custom app URL (for self-hosted instances) |
|
|
63
|
+
|
|
64
|
+
## Security
|
|
65
|
+
|
|
66
|
+
- Tokens are hashed before storage in the database
|
|
67
|
+
- Token file is stored with `0o600` permissions (owner-only)
|
|
68
|
+
- Auth callback uses POST (token never appears in URLs or browser history)
|
|
69
|
+
- All API calls go through the same validation and rate limiting as the web UI
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for app.dovito.com API routes.
|
|
3
|
+
* Authenticates via Bearer token (personal access token or legacy MCP_API_KEY).
|
|
4
|
+
*/
|
|
5
|
+
export declare class ApiClient {
|
|
6
|
+
private baseUrl;
|
|
7
|
+
private token;
|
|
8
|
+
constructor(baseUrl: string, token: string);
|
|
9
|
+
private request;
|
|
10
|
+
get<T>(path: string): Promise<T>;
|
|
11
|
+
post<T>(path: string, body: unknown): Promise<T>;
|
|
12
|
+
patch<T>(path: string, body: unknown): Promise<T>;
|
|
13
|
+
delete<T>(path: string): Promise<T>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for app.dovito.com API routes.
|
|
3
|
+
* Authenticates via Bearer token (personal access token or legacy MCP_API_KEY).
|
|
4
|
+
*/
|
|
5
|
+
export class ApiClient {
|
|
6
|
+
baseUrl;
|
|
7
|
+
token;
|
|
8
|
+
constructor(baseUrl, token) {
|
|
9
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
10
|
+
this.token = token;
|
|
11
|
+
}
|
|
12
|
+
async request(method, path, body) {
|
|
13
|
+
const url = `${this.baseUrl}${path}`;
|
|
14
|
+
const headers = {
|
|
15
|
+
Authorization: `Bearer ${this.token}`,
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
};
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method,
|
|
20
|
+
headers,
|
|
21
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
let msg;
|
|
26
|
+
try {
|
|
27
|
+
msg = JSON.parse(text).message || text;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
msg = text;
|
|
31
|
+
}
|
|
32
|
+
// Truncate error message to avoid leaking internal details
|
|
33
|
+
const safeMsg = msg.length > 500 ? msg.slice(0, 500) + "..." : msg;
|
|
34
|
+
throw new Error(`API ${method} ${path} returned ${res.status}: ${safeMsg}`);
|
|
35
|
+
}
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
if (!text)
|
|
38
|
+
return undefined;
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
get(path) {
|
|
42
|
+
return this.request("GET", path);
|
|
43
|
+
}
|
|
44
|
+
post(path, body) {
|
|
45
|
+
return this.request("POST", path, body);
|
|
46
|
+
}
|
|
47
|
+
patch(path, body) {
|
|
48
|
+
return this.request("PATCH", path, body);
|
|
49
|
+
}
|
|
50
|
+
delete(path) {
|
|
51
|
+
return this.request("DELETE", path);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the stored personal access token, or null if not found/expired.
|
|
3
|
+
*/
|
|
4
|
+
export declare function getStoredToken(): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Run the browser-based authentication flow:
|
|
7
|
+
* 1. Start a temporary local HTTP server
|
|
8
|
+
* 2. Open the browser to the auth page with a callback URL
|
|
9
|
+
* 3. User signs in on the web
|
|
10
|
+
* 4. App redirects back to the local server with the token
|
|
11
|
+
* 5. Save the token and return it
|
|
12
|
+
*/
|
|
13
|
+
export declare function authenticateViaBrowser(appUrl: string): Promise<string>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".systematics");
|
|
7
|
+
const TOKEN_FILE = join(CONFIG_DIR, "token");
|
|
8
|
+
/**
|
|
9
|
+
* Get the stored personal access token, or null if not found/expired.
|
|
10
|
+
*/
|
|
11
|
+
export function getStoredToken() {
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(TOKEN_FILE))
|
|
14
|
+
return null;
|
|
15
|
+
const content = readFileSync(TOKEN_FILE, "utf-8").trim();
|
|
16
|
+
if (!content.startsWith("pat_"))
|
|
17
|
+
return null;
|
|
18
|
+
return content;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Save a token to the local config directory.
|
|
26
|
+
*/
|
|
27
|
+
function saveToken(token) {
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
29
|
+
writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Open a URL in the default browser (cross-platform).
|
|
33
|
+
*/
|
|
34
|
+
function openBrowser(url) {
|
|
35
|
+
if (process.platform === "darwin") {
|
|
36
|
+
execFile("open", [url]);
|
|
37
|
+
}
|
|
38
|
+
else if (process.platform === "win32") {
|
|
39
|
+
execFile("cmd", ["/c", "start", "", url]);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
execFile("xdg-open", [url]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Run the browser-based authentication flow:
|
|
47
|
+
* 1. Start a temporary local HTTP server
|
|
48
|
+
* 2. Open the browser to the auth page with a callback URL
|
|
49
|
+
* 3. User signs in on the web
|
|
50
|
+
* 4. App redirects back to the local server with the token
|
|
51
|
+
* 5. Save the token and return it
|
|
52
|
+
*/
|
|
53
|
+
export async function authenticateViaBrowser(appUrl) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const server = createServer(async (req, res) => {
|
|
56
|
+
const url = new URL(req.url || "/", `http://localhost`);
|
|
57
|
+
// CORS headers for the POST from the auth page
|
|
58
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
59
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
60
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
61
|
+
if (req.method === "OPTIONS") {
|
|
62
|
+
res.writeHead(204);
|
|
63
|
+
res.end();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (url.pathname === "/callback") {
|
|
67
|
+
let token = null;
|
|
68
|
+
// Prefer POST body (token not exposed in URL/history)
|
|
69
|
+
if (req.method === "POST") {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
for await (const chunk of req)
|
|
72
|
+
chunks.push(Buffer.from(chunk));
|
|
73
|
+
try {
|
|
74
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
75
|
+
token = body.token || null;
|
|
76
|
+
}
|
|
77
|
+
catch { /* fall through */ }
|
|
78
|
+
}
|
|
79
|
+
// Fallback: GET with query param
|
|
80
|
+
if (!token) {
|
|
81
|
+
token = url.searchParams.get("token");
|
|
82
|
+
}
|
|
83
|
+
if (token && token.startsWith("pat_")) {
|
|
84
|
+
saveToken(token);
|
|
85
|
+
// Send a success page that auto-closes
|
|
86
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
87
|
+
res.end(`<!DOCTYPE html>
|
|
88
|
+
<html>
|
|
89
|
+
<head><title>Authenticated</title>
|
|
90
|
+
<style>
|
|
91
|
+
body { font-family: -apple-system, system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #FAFAF9; }
|
|
92
|
+
.card { text-align: center; padding: 3rem; background: white; border-radius: 12px; border: 1px solid #E8E5E0; max-width: 400px; }
|
|
93
|
+
h1 { font-size: 1.5rem; margin: 1rem 0 0.5rem; color: #1A1A1A; }
|
|
94
|
+
p { color: #5C5650; font-size: 0.9rem; }
|
|
95
|
+
.check { width: 48px; height: 48px; border-radius: 50%; background: #16A34A20; display: inline-flex; align-items: center; justify-content: center; }
|
|
96
|
+
.check svg { width: 24px; height: 24px; color: #16A34A; }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<div class="card">
|
|
101
|
+
<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
|
+
<h1>Connected to Systematics</h1>
|
|
103
|
+
<p>You can close this tab and return to Claude Code.</p>
|
|
104
|
+
</div>
|
|
105
|
+
</body>
|
|
106
|
+
</html>`);
|
|
107
|
+
server.close();
|
|
108
|
+
resolve(token);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
112
|
+
res.end("<h1>Authentication failed</h1><p>No valid token received.</p>");
|
|
113
|
+
server.close();
|
|
114
|
+
reject(new Error("No valid token received"));
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
res.writeHead(404);
|
|
119
|
+
res.end();
|
|
120
|
+
});
|
|
121
|
+
// Listen on a random available port
|
|
122
|
+
server.listen(0, "127.0.0.1", () => {
|
|
123
|
+
const addr = server.address();
|
|
124
|
+
if (!addr || typeof addr === "string") {
|
|
125
|
+
reject(new Error("Failed to start callback server"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const callbackUrl = `http://127.0.0.1:${addr.port}/callback`;
|
|
129
|
+
const authUrl = `${appUrl}/auth/mcp?callback=${encodeURIComponent(callbackUrl)}`;
|
|
130
|
+
// Write to stderr so Claude Code can show it (stdout is MCP protocol)
|
|
131
|
+
process.stderr.write(`\nOpening browser to authenticate with Systematics...\n`);
|
|
132
|
+
process.stderr.write(`If the browser doesn't open, visit: ${authUrl}\n\n`);
|
|
133
|
+
openBrowser(authUrl);
|
|
134
|
+
});
|
|
135
|
+
// Timeout after 5 minutes
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
server.close();
|
|
138
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
139
|
+
}, 5 * 60 * 1000);
|
|
140
|
+
});
|
|
141
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ApiClient } from "./api-client.js";
|
|
6
|
+
import { getStoredToken, authenticateViaBrowser } from "./auth.js";
|
|
7
|
+
// Validate DOVITO_APP_URL against allowed hostnames to prevent SSRF
|
|
8
|
+
const rawUrl = process.env.DOVITO_APP_URL || "https://app.dovito.com";
|
|
9
|
+
const parsedUrl = new URL(rawUrl);
|
|
10
|
+
const ALLOWED_HOSTS = ["app.dovito.com", "localhost", "127.0.0.1"];
|
|
11
|
+
if (!ALLOWED_HOSTS.includes(parsedUrl.hostname)) {
|
|
12
|
+
process.stderr.write(`DOVITO_APP_URL hostname "${parsedUrl.hostname}" is not in the allowlist: ${ALLOWED_HOSTS.join(", ")}\n`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const BASE_URL = parsedUrl.origin;
|
|
16
|
+
// Resolve token: env var > stored token > browser auth flow
|
|
17
|
+
async function resolveToken() {
|
|
18
|
+
// 1. Explicit env var (highest priority)
|
|
19
|
+
const envToken = process.env.SYSTEMATICS_TOKEN || process.env.MCP_API_KEY;
|
|
20
|
+
if (envToken)
|
|
21
|
+
return envToken;
|
|
22
|
+
// 2. Previously stored token from browser auth
|
|
23
|
+
const stored = getStoredToken();
|
|
24
|
+
if (stored)
|
|
25
|
+
return stored;
|
|
26
|
+
// 3. Run browser auth flow
|
|
27
|
+
return authenticateViaBrowser(BASE_URL);
|
|
28
|
+
}
|
|
29
|
+
const token = await resolveToken();
|
|
30
|
+
const api = new ApiClient(BASE_URL, token);
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: "dovito",
|
|
33
|
+
version: "1.0.0",
|
|
34
|
+
});
|
|
35
|
+
// ── Businesses ─────────────────────────────────────────────────────────────
|
|
36
|
+
server.tool("list_businesses", "List all businesses (clients) in the Dovito portal", {}, async () => {
|
|
37
|
+
const data = await api.get("/api/businesses");
|
|
38
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
39
|
+
});
|
|
40
|
+
server.tool("get_business", "Get a single business by ID", { businessId: z.string().describe("Business UUID") }, async ({ businessId }) => {
|
|
41
|
+
const data = await api.get(`/api/businesses/${businessId}`);
|
|
42
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
43
|
+
});
|
|
44
|
+
server.tool("create_business", "Create a new business (client company)", {
|
|
45
|
+
name: z.string().describe("Business name (required)"),
|
|
46
|
+
industry: z.string().optional().describe("Industry"),
|
|
47
|
+
description: z.string().optional().describe("Description"),
|
|
48
|
+
website: z.string().optional().describe("Website URL"),
|
|
49
|
+
phone: z.string().optional().describe("Phone number"),
|
|
50
|
+
address: z.string().optional().describe("Address"),
|
|
51
|
+
status: z.enum(["pending", "approved", "active"]).optional(),
|
|
52
|
+
ownerUserId: z.string().optional().describe("User ID of the business owner, or 'none'"),
|
|
53
|
+
}, async (params) => {
|
|
54
|
+
const data = await api.post("/api/businesses", params);
|
|
55
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
56
|
+
});
|
|
57
|
+
server.tool("update_business", "Update an existing business", {
|
|
58
|
+
businessId: z.string().describe("Business UUID"),
|
|
59
|
+
name: z.string().optional(),
|
|
60
|
+
legalName: z.string().nullable().optional(),
|
|
61
|
+
website: z.string().nullable().optional(),
|
|
62
|
+
industry: z.string().nullable().optional(),
|
|
63
|
+
description: z.string().nullable().optional(),
|
|
64
|
+
phone: z.string().nullable().optional(),
|
|
65
|
+
address: z.string().nullable().optional(),
|
|
66
|
+
status: z.enum(["pending", "approved", "active"]).optional(),
|
|
67
|
+
assignedAmId: z.string().nullable().optional().describe("Account manager user ID"),
|
|
68
|
+
}, async ({ businessId, ...updates }) => {
|
|
69
|
+
const data = await api.patch(`/api/businesses/${businessId}`, updates);
|
|
70
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
71
|
+
});
|
|
72
|
+
// ── Projects ───────────────────────────────────────────────────────────────
|
|
73
|
+
server.tool("list_projects", "List all projects across businesses", {}, async () => {
|
|
74
|
+
const data = await api.get("/api/projects");
|
|
75
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
76
|
+
});
|
|
77
|
+
server.tool("get_project", "Get a single project by ID", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
78
|
+
const data = await api.get(`/api/projects/${projectId}`);
|
|
79
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
80
|
+
});
|
|
81
|
+
server.tool("create_project", "Create a new project under a business", {
|
|
82
|
+
name: z.string().describe("Project name"),
|
|
83
|
+
businessId: z.string().describe("Business UUID"),
|
|
84
|
+
description: z.string().optional(),
|
|
85
|
+
status: z.enum(["planning", "in_progress", "on_hold", "completed", "cancelled"]).optional(),
|
|
86
|
+
serviceType: z.string().optional(),
|
|
87
|
+
phaseName: z.string().optional(),
|
|
88
|
+
contractValue: z.number().optional().describe("Value in cents"),
|
|
89
|
+
startDate: z.string().optional().describe("ISO date"),
|
|
90
|
+
estimatedEndDate: z.string().optional().describe("ISO date"),
|
|
91
|
+
}, async (params) => {
|
|
92
|
+
const data = await api.post("/api/projects", params);
|
|
93
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
94
|
+
});
|
|
95
|
+
server.tool("update_project", "Update an existing project", {
|
|
96
|
+
projectId: z.string().describe("Project UUID"),
|
|
97
|
+
name: z.string().optional(),
|
|
98
|
+
description: z.string().optional(),
|
|
99
|
+
status: z.enum(["planning", "in_progress", "on_hold", "completed", "cancelled"]).optional(),
|
|
100
|
+
progress: z.number().min(0).max(100).optional(),
|
|
101
|
+
contractValue: z.number().optional().describe("Value in cents"),
|
|
102
|
+
startDate: z.string().optional().describe("ISO date"),
|
|
103
|
+
estimatedEndDate: z.string().optional().describe("ISO date"),
|
|
104
|
+
}, async ({ projectId, ...updates }) => {
|
|
105
|
+
const data = await api.patch(`/api/projects/${projectId}`, updates);
|
|
106
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
107
|
+
});
|
|
108
|
+
// ── Tasks ──────────────────────────────────────────────────────────────────
|
|
109
|
+
server.tool("list_tasks", "List all tasks, optionally filtered by business", {
|
|
110
|
+
businessId: z.string().optional().describe("Filter by business UUID"),
|
|
111
|
+
}, async ({ businessId }) => {
|
|
112
|
+
const params = new URLSearchParams();
|
|
113
|
+
if (businessId)
|
|
114
|
+
params.set("businessId", businessId);
|
|
115
|
+
const query = params.toString() ? `?${params}` : "";
|
|
116
|
+
const data = await api.get(`/api/tasks${query}`);
|
|
117
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
118
|
+
});
|
|
119
|
+
server.tool("list_project_tasks", "List tasks for a specific project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
120
|
+
const data = await api.get(`/api/projects/${projectId}/tasks`);
|
|
121
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
122
|
+
});
|
|
123
|
+
server.tool("create_task", "Create a task under a project", {
|
|
124
|
+
projectId: z.string().describe("Project UUID"),
|
|
125
|
+
title: z.string().describe("Task title (1-255 chars)"),
|
|
126
|
+
description: z.string().optional(),
|
|
127
|
+
type: z.enum(["general", "resource_request", "proposal_signing", "deliverable_review", "onboarding"]).optional(),
|
|
128
|
+
assignedToId: z.string().optional().describe("User UUID to assign to"),
|
|
129
|
+
startDate: z.string().optional().describe("ISO date"),
|
|
130
|
+
dueDate: z.string().optional().describe("ISO date"),
|
|
131
|
+
}, async ({ projectId, ...body }) => {
|
|
132
|
+
const data = await api.post(`/api/projects/${projectId}/tasks`, body);
|
|
133
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
134
|
+
});
|
|
135
|
+
server.tool("update_task", "Update an existing task", {
|
|
136
|
+
projectId: z.string().describe("Project UUID"),
|
|
137
|
+
taskId: z.string().describe("Task UUID"),
|
|
138
|
+
title: z.string().optional(),
|
|
139
|
+
description: z.string().optional(),
|
|
140
|
+
status: z.enum(["pending", "in_progress", "done", "completed", "cancelled"]).optional(),
|
|
141
|
+
type: z.enum(["general", "resource_request", "proposal_signing", "deliverable_review", "onboarding"]).optional(),
|
|
142
|
+
assignedToId: z.string().optional(),
|
|
143
|
+
startDate: z.string().optional().describe("ISO date"),
|
|
144
|
+
dueDate: z.string().optional().describe("ISO date"),
|
|
145
|
+
}, async ({ projectId, taskId, ...updates }) => {
|
|
146
|
+
const data = await api.patch(`/api/projects/${projectId}/tasks/${taskId}`, updates);
|
|
147
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
148
|
+
});
|
|
149
|
+
server.tool("delete_task", "Delete a task (set confirm=true to proceed)", {
|
|
150
|
+
projectId: z.string().describe("Project UUID"),
|
|
151
|
+
taskId: z.string().describe("Task UUID"),
|
|
152
|
+
confirm: z.literal(true).describe("Must be true to confirm deletion"),
|
|
153
|
+
}, async ({ projectId, taskId }) => {
|
|
154
|
+
await api.delete(`/api/projects/${projectId}/tasks/${taskId}`);
|
|
155
|
+
return { content: [{ type: "text", text: "Task deleted." }] };
|
|
156
|
+
});
|
|
157
|
+
// ── Catalog ────────────────────────────────────────────────────────────────
|
|
158
|
+
server.tool("list_catalog_items", "List all items in the line item catalog", { all: z.boolean().optional().describe("Include inactive items") }, async ({ all }) => {
|
|
159
|
+
const query = all ? "?all=true" : "";
|
|
160
|
+
const data = await api.get(`/api/catalog${query}`);
|
|
161
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
162
|
+
});
|
|
163
|
+
server.tool("create_catalog_item", "Add an item to the line item catalog", {
|
|
164
|
+
name: z.string().describe("Item name"),
|
|
165
|
+
description: z.string().optional(),
|
|
166
|
+
costCode: z.string().optional().describe("Cost code e.g. SVC-001"),
|
|
167
|
+
category: z.enum(["service", "product", "digital", "physical"]).optional(),
|
|
168
|
+
unitAmount: z.number().int().min(0).describe("Price in cents"),
|
|
169
|
+
unit: z.string().optional().describe("hour, each, month, project, per seat, quarterly, annual"),
|
|
170
|
+
}, async (params) => {
|
|
171
|
+
const data = await api.post("/api/catalog", params);
|
|
172
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
173
|
+
});
|
|
174
|
+
server.tool("update_catalog_item", "Update a catalog item", {
|
|
175
|
+
itemId: z.string().describe("Catalog item UUID"),
|
|
176
|
+
name: z.string().optional(),
|
|
177
|
+
description: z.string().optional(),
|
|
178
|
+
costCode: z.string().optional(),
|
|
179
|
+
category: z.enum(["service", "product", "digital", "physical"]).optional(),
|
|
180
|
+
unitAmount: z.number().int().min(0).optional(),
|
|
181
|
+
unit: z.string().optional(),
|
|
182
|
+
isActive: z.boolean().optional(),
|
|
183
|
+
}, async ({ itemId, ...updates }) => {
|
|
184
|
+
const data = await api.patch(`/api/catalog/${itemId}`, updates);
|
|
185
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
186
|
+
});
|
|
187
|
+
// ── Users ──────────────────────────────────────────────────────────────────
|
|
188
|
+
server.tool("list_users", "List all users in the portal", {
|
|
189
|
+
businessId: z.string().optional().describe("Filter by business UUID"),
|
|
190
|
+
includeStaff: z.boolean().optional().describe("Include staff/admin users"),
|
|
191
|
+
}, async ({ businessId, includeStaff }) => {
|
|
192
|
+
const params = new URLSearchParams();
|
|
193
|
+
if (businessId)
|
|
194
|
+
params.set("businessId", businessId);
|
|
195
|
+
if (includeStaff)
|
|
196
|
+
params.set("includeStaff", "true");
|
|
197
|
+
const query = params.toString() ? `?${params}` : "";
|
|
198
|
+
const data = await api.get(`/api/admin/users${query}`);
|
|
199
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
200
|
+
});
|
|
201
|
+
// ── Proposals ──────────────────────────────────────────────────────────────
|
|
202
|
+
server.tool("list_proposals", "List proposals for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
203
|
+
const data = await api.get(`/api/projects/${projectId}/proposals`);
|
|
204
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
205
|
+
});
|
|
206
|
+
server.tool("create_proposal", "Create a proposal (quote) for a project", {
|
|
207
|
+
projectId: z.string().describe("Project UUID"),
|
|
208
|
+
title: z.string().describe("Proposal title (1-200 chars)"),
|
|
209
|
+
description: z.string().optional().describe("Description (max 2000 chars)"),
|
|
210
|
+
lineItems: z.array(z.object({
|
|
211
|
+
description: z.string(),
|
|
212
|
+
quantity: z.number().int().min(1),
|
|
213
|
+
unitAmount: z.number().int().min(0).describe("Amount in cents"),
|
|
214
|
+
})).describe("Line items for the proposal"),
|
|
215
|
+
expiresAt: z.string().optional().describe("ISO date for expiration"),
|
|
216
|
+
}, async ({ projectId, ...body }) => {
|
|
217
|
+
const data = await api.post(`/api/projects/${projectId}/proposals`, body);
|
|
218
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
219
|
+
});
|
|
220
|
+
// ── Conversations & Messages ───────────────────────────────────────────────
|
|
221
|
+
server.tool("list_conversations", "List all conversations (message threads) for the service account", {}, async () => {
|
|
222
|
+
const data = await api.get("/api/messages");
|
|
223
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
224
|
+
});
|
|
225
|
+
server.tool("get_conversation_messages", "Get messages in a conversation thread", {
|
|
226
|
+
conversationId: z.string().describe("Conversation UUID"),
|
|
227
|
+
cursor: z.string().optional().describe("Pagination cursor (message ID)"),
|
|
228
|
+
}, async ({ conversationId, cursor }) => {
|
|
229
|
+
const params = new URLSearchParams();
|
|
230
|
+
if (cursor)
|
|
231
|
+
params.set("cursor", cursor);
|
|
232
|
+
const query = params.toString() ? `?${params}` : "";
|
|
233
|
+
const data = await api.get(`/api/messages/${conversationId}${query}`);
|
|
234
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
235
|
+
});
|
|
236
|
+
server.tool("create_conversation", "Create a new conversation (DM, group, project, or business thread)", {
|
|
237
|
+
type: z.enum(["direct", "group", "project", "business"]).describe("Conversation type"),
|
|
238
|
+
participantIds: z.array(z.string()).optional().describe("User UUIDs to include"),
|
|
239
|
+
name: z.string().optional().describe("Group conversation name"),
|
|
240
|
+
projectId: z.string().optional().describe("Project UUID (for project type)"),
|
|
241
|
+
businessId: z.string().optional().describe("Business UUID (for business type)"),
|
|
242
|
+
}, async (params) => {
|
|
243
|
+
const data = await api.post("/api/conversations", params);
|
|
244
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
245
|
+
});
|
|
246
|
+
server.tool("send_message", "Send a message in a conversation", {
|
|
247
|
+
conversationId: z.string().describe("Conversation UUID"),
|
|
248
|
+
content: z.string().describe("Message content (1-10000 chars)"),
|
|
249
|
+
parentMessageId: z.string().optional().describe("Reply to a specific message"),
|
|
250
|
+
}, async ({ conversationId, ...body }) => {
|
|
251
|
+
const data = await api.post(`/api/messages/${conversationId}`, body);
|
|
252
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
253
|
+
});
|
|
254
|
+
// ── Deliverables ───────────────────────────────────────────────────────────
|
|
255
|
+
server.tool("list_deliverables", "List deliverables for a project", { projectId: z.string().describe("Project UUID") }, async ({ projectId }) => {
|
|
256
|
+
const data = await api.get(`/api/projects/${projectId}/deliverables`);
|
|
257
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
258
|
+
});
|
|
259
|
+
server.tool("create_deliverable", "Create a deliverable under a project", {
|
|
260
|
+
projectId: z.string().describe("Project UUID"),
|
|
261
|
+
title: z.string().describe("Deliverable title"),
|
|
262
|
+
description: z.string().optional(),
|
|
263
|
+
status: z.enum(["pending", "in_progress", "completed"]).optional(),
|
|
264
|
+
}, async ({ projectId, ...body }) => {
|
|
265
|
+
const data = await api.post(`/api/projects/${projectId}/deliverables`, body);
|
|
266
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
267
|
+
});
|
|
268
|
+
server.tool("update_deliverable", "Update a deliverable", {
|
|
269
|
+
projectId: z.string().describe("Project UUID"),
|
|
270
|
+
deliverableId: z.string().describe("Deliverable UUID"),
|
|
271
|
+
title: z.string().optional(),
|
|
272
|
+
description: z.string().optional(),
|
|
273
|
+
status: z.enum(["pending", "in_progress", "completed"]).optional(),
|
|
274
|
+
}, async ({ projectId, deliverableId, ...updates }) => {
|
|
275
|
+
const data = await api.patch(`/api/projects/${projectId}/deliverables/${deliverableId}`, updates);
|
|
276
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
277
|
+
});
|
|
278
|
+
// ── Applications (Pipeline) ───────────────────────────────────────────────
|
|
279
|
+
server.tool("list_applications", "List all applications in the pipeline", {}, async () => {
|
|
280
|
+
const data = await api.get("/api/admin/applications");
|
|
281
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
282
|
+
});
|
|
283
|
+
// ── Start server ───────────────────────────────────────────────────────────
|
|
284
|
+
async function main() {
|
|
285
|
+
const transport = new StdioServerTransport();
|
|
286
|
+
await server.connect(transport);
|
|
287
|
+
}
|
|
288
|
+
main().catch((err) => {
|
|
289
|
+
console.error("Fatal:", err);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "systematics-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Claude Code MCP server for the Systematics platform by Dovito Business Solutions",
|
|
6
|
+
"bin": {
|
|
7
|
+
"systematics-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "tsx src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"tsx": "^4.20.5",
|
|
21
|
+
"typescript": "^5.6.3",
|
|
22
|
+
"@types/node": "^20.16.11"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**/*",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"keywords": ["mcp", "claude", "claude-code", "systematics", "dovito", "model-context-protocol"],
|
|
29
|
+
"author": "Dovito Business Solutions",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/dovito-dev/app.dovito.com.git",
|
|
34
|
+
"directory": "mcp-server"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://app.dovito.com"
|
|
37
|
+
}
|