systematics-mcp 1.0.1 → 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 +8 -3
- 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,11 +17,16 @@ 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
|
+
// On 401, clear stored token so next call re-authenticates
|
|
21
|
+
function handleAuthError() {
|
|
22
|
+
clearStoredToken();
|
|
23
|
+
api = null;
|
|
24
|
+
}
|
|
20
25
|
// Lazy API client -- initialized on first use or after auth
|
|
21
26
|
let api = null;
|
|
22
27
|
const token = getToken();
|
|
23
28
|
if (token) {
|
|
24
|
-
api = new ApiClient(BASE_URL, token);
|
|
29
|
+
api = new ApiClient(BASE_URL, token, handleAuthError);
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* Get the API client, triggering browser auth if not yet authenticated.
|
|
@@ -31,7 +36,7 @@ async function getApi() {
|
|
|
31
36
|
if (api)
|
|
32
37
|
return api;
|
|
33
38
|
const newToken = await authenticateViaBrowser(BASE_URL);
|
|
34
|
-
api = new ApiClient(BASE_URL, newToken);
|
|
39
|
+
api = new ApiClient(BASE_URL, newToken, handleAuthError);
|
|
35
40
|
return api;
|
|
36
41
|
}
|
|
37
42
|
const server = new McpServer({
|