@systemdox/mcp-server 0.1.54 → 0.1.56

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 PuglieseWeb LTD
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -25,8 +25,10 @@ MCP-compatible tool read that context — and write decisions back.
25
25
 
26
26
  1. SystemDox is in private preview. [Request early access](https://www.systemdox.com)
27
27
  to get a workspace.
28
- 2. Connect a GitHub repository and generate an API key (workspace settings).
29
- 3. Add the server to your tool — hosted (Option A) or local (Option B) below.
28
+ 2. Connect a GitHub repository.
29
+ 3. Paste `https://<your-workspace>.mcp.systemdox.com/connect` into Claude and
30
+ sign in, or generate an API key (workspace settings) and add the server to
31
+ your tool — hosted (Option A) or local (Option B) below.
30
32
 
31
33
  Docs: [MCP setup guide](https://www.systemdox.com/guides/mcp-setup) ·
32
34
  [Documentation standard](https://www.systemdox.com/guides/documentation-standard) ·
@@ -34,9 +36,27 @@ Docs: [MCP setup guide](https://www.systemdox.com/guides/mcp-setup) ·
34
36
 
35
37
  ---
36
38
 
37
- There are two ways to run it. Both authenticate with a SystemDox API key
38
- (generate one on your SystemDox settings page) and act strictly as the caller —
39
- every tool call is scoped to that key's tenant.
39
+ ## Connect by signing in
40
+
41
+ Your workspace has its own address. Paste it into Claude's Settings → Connectors,
42
+ or run:
43
+
44
+ ```bash
45
+ claude mcp add --transport http systemdox https://<your-workspace>.mcp.systemdox.com/connect
46
+ ```
47
+
48
+ Your browser opens SystemDox. Sign in with your second factor and approve. It only
49
+ reads unless you choose to allow writes. Claude holds a token for an hour, renewed
50
+ for you, and the API key behind the connection stays with SystemDox. Only an owner
51
+ or admin of the workspace can connect this way for now, and an account that signs
52
+ in only with Google or Apple cannot.
53
+
54
+ ## With an API key
55
+
56
+ There are two more ways to run it, for scripts, CI and other tools. Both
57
+ authenticate with a SystemDox API key (generate one on your SystemDox settings
58
+ page) and act strictly as the caller — every tool call is scoped to that key's
59
+ tenant.
40
60
 
41
61
  ## Option A — Hosted endpoint (no install)
42
62
 
@@ -323,6 +343,18 @@ permission allowlist that pins them keeps working unchanged.
323
343
  Removal also requires usage telemetry showing no calls against the old name.
324
344
  The `/guardrails/*` REST routes are **not** deprecated and are not changing.
325
345
 
346
+ ## Source and licence
347
+
348
+ The source is public at
349
+ [puglieseweb/systemdox-mcp-server](https://github.com/puglieseweb/systemdox-mcp-server),
350
+ a read-only mirror of this package. It is developed in the SystemDox monorepo,
351
+ and every change that reaches its main branch is copied to the mirror as one
352
+ commit. Issues are welcome on the mirror; a pull request there is read, and
353
+ any change it proposes is made in the monorepo. In the mirror, `npm ci` and
354
+ `npm run build` build the package on its own, and `npm test` runs its tests;
355
+ three of them read shared examples from the monorepo's `specs/` and pass only
356
+ there. ISC licence, © 2026 PuglieseWeb LTD.
357
+
326
358
  ## Development
327
359
 
328
360
  ```bash
package/dist/cli.js CHANGED
@@ -267,6 +267,17 @@ var init_cache = __esm({
267
267
  });
268
268
 
269
269
  // src/lib/api-client.ts
270
+ function unescapedPath(path) {
271
+ let current = path.split(/[?#]/, 1)[0];
272
+ for (; ; ) {
273
+ const next = current.replace(/[\t\n\r]/g, "").replace(/%(?:2e|2f|5c|25|09|0a|0d)/gi, (escape) => decodeURIComponent(escape));
274
+ if (next === current) return current;
275
+ current = next;
276
+ }
277
+ }
278
+ function walksThePath(path) {
279
+ return unescapedPath(path).split(/[/\\]/).some((segment) => segment === "." || segment === "..");
280
+ }
270
281
  var SystemDoxApiError, TEAM_SETUP, seg, SystemDoxApiClient;
271
282
  var init_api_client = __esm({
272
283
  "src/lib/api-client.ts"() {
@@ -327,7 +338,7 @@ var init_api_client = __esm({
327
338
  }
328
339
  async mutate(method, path, body) {
329
340
  const url = this.buildUrl(path);
330
- const res = await fetch(url, {
341
+ const res = await this.call(url, {
331
342
  method,
332
343
  headers: {
333
344
  // The API Gateway authorizer only reads the Authorization header;
@@ -377,7 +388,7 @@ var init_api_client = __esm({
377
388
  /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
378
389
  * header. The caller checks the BYTES against the hash it was given. */
379
390
  async getPluginZip(workspace, version) {
380
- const res = await fetch(
391
+ const res = await this.call(
381
392
  this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
382
393
  {
383
394
  headers: {
@@ -443,6 +454,10 @@ var init_api_client = __esm({
443
454
  buildUrl(path, params) {
444
455
  const base = this.config.apiUrl.replace(/\/+$/, "");
445
456
  const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
457
+ const { origin, pathname } = new URL(base);
458
+ if (walksThePath(path) || url.origin !== origin || !url.pathname.startsWith(`${pathname.replace(/\/+$/, "")}/`)) {
459
+ throw new Error("Refused: this id leaves the API's own path.");
460
+ }
446
461
  if (params) {
447
462
  for (const [key, value] of Object.entries(params)) {
448
463
  url.searchParams.set(key, value);
@@ -452,7 +467,7 @@ var init_api_client = __esm({
452
467
  }
453
468
  async fetchRaw(path, params, signal) {
454
469
  const url = this.buildUrl(path, params);
455
- return fetch(url, {
470
+ return this.call(url, {
456
471
  headers: {
457
472
  Authorization: `Bearer ${this.config.apiKey}`,
458
473
  "X-API-Key": this.config.apiKey,
@@ -461,6 +476,16 @@ var init_api_client = __esm({
461
476
  signal
462
477
  });
463
478
  }
479
+ /** Every call of this client, so `onAnswer` hears each answer. */
480
+ async call(url, init) {
481
+ const res = await fetch(url, init);
482
+ const { onAnswer } = this.config;
483
+ if (onAnswer) {
484
+ const refused2 = res.ok ? void 0 : new SystemDoxApiError(res.status, await res.clone().text());
485
+ onAnswer({ status: res.status, code: refused2?.code });
486
+ }
487
+ return res;
488
+ }
464
489
  };
465
490
  }
466
491
  });
@@ -2889,7 +2914,7 @@ function registerGetSpecs(server, apiClient2) {
2889
2914
  async ({ draft_id, status, search }) => {
2890
2915
  if (draft_id) {
2891
2916
  const spec = await apiClient2.get(
2892
- `/v1/specs/${draft_id}`
2917
+ `/v1/specs/${encodeURIComponent(draft_id)}`
2893
2918
  );
2894
2919
  const sections = [`# ${spec.name}
2895
2920
 
@@ -4617,7 +4642,8 @@ function registerUpdateSpec(server, apiClient2) {
4617
4642
  }
4618
4643
  },
4619
4644
  async ({ draft_id, section, content, requirement_name }) => {
4620
- const spec = await apiClient2.get(`/v1/specs/${draft_id}`);
4645
+ const specPath = `/v1/specs/${encodeURIComponent(draft_id)}`;
4646
+ const spec = await apiClient2.get(specPath);
4621
4647
  if (spec.status !== "draft") {
4622
4648
  return {
4623
4649
  content: [
@@ -4642,8 +4668,8 @@ function registerUpdateSpec(server, apiClient2) {
4642
4668
  };
4643
4669
  body.requirements = [...existing, newReq];
4644
4670
  }
4645
- await apiClient2.put(`/v1/specs/${draft_id}`, body);
4646
- apiClient2.invalidateCache(`/v1/specs/${draft_id}`);
4671
+ await apiClient2.put(specPath, body);
4672
+ apiClient2.invalidateCache(specPath);
4647
4673
  apiClient2.invalidateCache("/v1/specs");
4648
4674
  return {
4649
4675
  content: [
@@ -4707,7 +4733,7 @@ ${decision}
4707
4733
  ## Consequences
4708
4734
 
4709
4735
  ${consequences}` + relatedSection;
4710
- await apiClient2.put(`/v1/specs/${created.draftId}`, {
4736
+ await apiClient2.put(`/v1/specs/${encodeURIComponent(created.draftId)}`, {
4711
4737
  proposalContent: adrContent
4712
4738
  });
4713
4739
  apiClient2.invalidateCache("/v1/specs");
@@ -5294,14 +5320,25 @@ var init_team_setup = __esm({
5294
5320
 
5295
5321
  // src/server.ts
5296
5322
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5297
- function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
5298
- const server = new McpServer(
5323
+ function offering(server, access) {
5324
+ const offered = new Set(access === "write" ? [...READ_TOOLS, ...WRITE_TOOLS] : READ_TOOLS);
5325
+ return new Proxy(server, {
5326
+ get(target, property) {
5327
+ const value = Reflect.get(target, property, target);
5328
+ if (property !== "registerTool") return typeof value === "function" ? value.bind(target) : value;
5329
+ return (name, ...rest) => offered.has(name) ? target.registerTool(name, ...rest) : void 0;
5330
+ }
5331
+ });
5332
+ }
5333
+ function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2, access } = {}) {
5334
+ const mcpServer = new McpServer(
5299
5335
  {
5300
5336
  name: "systemdox-mcp-server",
5301
5337
  version: SERVER_VERSION
5302
5338
  },
5303
5339
  instructions ? { instructions } : void 0
5304
5340
  );
5341
+ const server = access ? offering(mcpServer, access) : mcpServer;
5305
5342
  registerGetGuardrails(server, apiClient2);
5306
5343
  registerGetSpecs(server, apiClient2);
5307
5344
  registerGetDocs(server, apiClient2);
@@ -5321,9 +5358,9 @@ function createServer(apiClient2, { localFiles = false, instructions, keyProblem
5321
5358
  registerCreatePromptTemplate(server, apiClient2);
5322
5359
  registerSkillTools(server, apiClient2, { localFiles });
5323
5360
  registerTeamSetupTools(server, apiClient2, { localFiles });
5324
- return server;
5361
+ return mcpServer;
5325
5362
  }
5326
- var SERVER_VERSION;
5363
+ var SERVER_VERSION, READ_TOOLS, WRITE_TOOLS;
5327
5364
  var init_server = __esm({
5328
5365
  "src/server.ts"() {
5329
5366
  "use strict";
@@ -5346,7 +5383,37 @@ var init_server = __esm({
5346
5383
  init_create_prompt_template();
5347
5384
  init_skills2();
5348
5385
  init_team_setup();
5349
- SERVER_VERSION = true ? "0.1.54" : "0.0.0-dev";
5386
+ SERVER_VERSION = true ? "0.1.56" : "0.0.0-dev";
5387
+ READ_TOOLS = [
5388
+ "get_context",
5389
+ "get_checks",
5390
+ "get_guardrails",
5391
+ "get_project_rules",
5392
+ "get_prompt_templates",
5393
+ "get_specs",
5394
+ "get_docs",
5395
+ "get_tech_stack",
5396
+ "get_planning_context",
5397
+ "get_plan_board",
5398
+ "get_change",
5399
+ "get_team_setup",
5400
+ "get_document_links",
5401
+ "trace_document_path",
5402
+ "get_graph_stats",
5403
+ "list_skills",
5404
+ "install_skill",
5405
+ "resolve_person"
5406
+ ];
5407
+ WRITE_TOOLS = [
5408
+ "create_check",
5409
+ "create_guardrail",
5410
+ "create_project_rule",
5411
+ "create_prompt_template",
5412
+ "update_spec",
5413
+ "report_decision",
5414
+ "publish_skill",
5415
+ "publish_setup_part"
5416
+ ];
5350
5417
  }
5351
5418
  });
5352
5419