@systemdox/mcp-server 0.1.48 → 0.1.50
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 +53 -11
- package/dist/cli.js +963 -357
- package/dist/http.js +1248 -732
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1341 -784
- package/dist/install-skill.js +10 -17
- package/dist/lib/api-client.d.ts +2 -3
- package/dist/lib/api-client.js +7 -14
- package/dist/lib/config.d.ts +4 -1
- package/dist/lib/config.js +1 -2
- package/dist/lib/context-builder.js +14 -7
- package/dist/lib/session-brief.d.ts +59 -0
- package/dist/lib/session-brief.js +174 -0
- package/dist/lib/skills.js +2 -1
- package/dist/lib/team-setup-files.d.ts +14 -1
- package/dist/lib/team-setup-files.js +43 -0
- package/dist/lib/team-setup-types.d.ts +18 -0
- package/dist/plugin-path.js +7 -14
- package/dist/server.d.ts +5 -1
- package/dist/server.js +3579 -0
- package/dist/setup-mcp.d.ts +66 -0
- package/dist/setup-mcp.js +630 -0
- package/dist/setup-team.d.ts +5 -1
- package/dist/setup-team.js +8 -14
- package/dist/sync-memory.js +29 -26
- package/dist/tools/document-graph.d.ts +87 -0
- package/dist/tools/get-change.d.ts +134 -0
- package/dist/tools/get-change.js +147 -0
- package/dist/tools/get-team-setup.d.ts +6 -0
- package/dist/tools/skills.js +3 -2
- package/dist/tools/team-setup.js +2 -1
- package/package.json +1 -1
package/dist/setup-team.d.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* 4. Finish: the two steps a person must take in Claude Code.
|
|
18
18
|
*
|
|
19
19
|
* It never runs `claude plugin marketplace add` and never sets
|
|
20
|
-
* `enabledPlugins`. The key is written to credentials.json only
|
|
20
|
+
* `enabledPlugins`. The key is written to credentials.json only (the MCP
|
|
21
|
+
* server step, setup-mcp.ts, has stored it there already).
|
|
21
22
|
*
|
|
22
23
|
* Exit codes: 0 done; 1 usage, no key or the API refused; 2 an edited team
|
|
23
24
|
* CLAUDE.md would be replaced; 3 Claude Code missing or too old; 4 not
|
|
@@ -60,6 +61,9 @@ export declare function claudeCodeVersion(): Promise<string | undefined>;
|
|
|
60
61
|
* last one in the file's own indentation and line ending.
|
|
61
62
|
*/
|
|
62
63
|
export declare function setJsonMember(text: string, keys: string[], value: unknown): string;
|
|
64
|
+
/** Set `keys` to `value` in the JSON file at `path`, creating it when absent;
|
|
65
|
+
* nothing is written when it already holds that value. */
|
|
66
|
+
export declare function setInJsonFile(path: string, keys: string[], value: unknown): Promise<void>;
|
|
63
67
|
export declare function importBlock(workspace: string, eol?: string): string;
|
|
64
68
|
/** `text` with the workspace's marked block added at the end, or replaced
|
|
65
69
|
* where it is; nothing outside the block changes. */
|
package/dist/setup-team.js
CHANGED
|
@@ -68,14 +68,6 @@ var SystemDoxApiClient = class {
|
|
|
68
68
|
this.config = config;
|
|
69
69
|
this.cache = new ResponseCache(config.cacheTtlMs);
|
|
70
70
|
}
|
|
71
|
-
async validate() {
|
|
72
|
-
const res = await this.fetchRaw("/v1/health");
|
|
73
|
-
if (!res.ok) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
71
|
async get(path, params) {
|
|
80
72
|
const url = this.buildUrl(path, params);
|
|
81
73
|
const cacheKey = url.toString();
|
|
@@ -89,8 +81,8 @@ var SystemDoxApiClient = class {
|
|
|
89
81
|
}
|
|
90
82
|
/** A read that skips the cache: an install must take the version that is
|
|
91
83
|
* latest now, not the one cached before someone published another. */
|
|
92
|
-
async getFresh(path) {
|
|
93
|
-
const res = await this.fetchRaw(path);
|
|
84
|
+
async getFresh(path, signal) {
|
|
85
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
94
86
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
95
87
|
return await res.json();
|
|
96
88
|
}
|
|
@@ -120,8 +112,8 @@ var SystemDoxApiClient = class {
|
|
|
120
112
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
121
113
|
// plugin-path act on what the workspace holds now.
|
|
122
114
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
123
|
-
getOwnTeamSetup() {
|
|
124
|
-
return this.getFresh(TEAM_SETUP);
|
|
115
|
+
getOwnTeamSetup(signal) {
|
|
116
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
125
117
|
}
|
|
126
118
|
getTeamSetup(workspace) {
|
|
127
119
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -225,14 +217,15 @@ var SystemDoxApiClient = class {
|
|
|
225
217
|
}
|
|
226
218
|
return url;
|
|
227
219
|
}
|
|
228
|
-
async fetchRaw(path, params) {
|
|
220
|
+
async fetchRaw(path, params, signal) {
|
|
229
221
|
const url = this.buildUrl(path, params);
|
|
230
222
|
return fetch(url, {
|
|
231
223
|
headers: {
|
|
232
224
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
233
225
|
"X-API-Key": this.config.apiKey,
|
|
234
226
|
Accept: "application/json"
|
|
235
|
-
}
|
|
227
|
+
},
|
|
228
|
+
signal
|
|
236
229
|
});
|
|
237
230
|
}
|
|
238
231
|
};
|
|
@@ -1588,6 +1581,7 @@ export {
|
|
|
1588
1581
|
parseSetupArgs,
|
|
1589
1582
|
runTeamSetup,
|
|
1590
1583
|
runTeamSetupCommand,
|
|
1584
|
+
setInJsonFile,
|
|
1591
1585
|
setJsonMember,
|
|
1592
1586
|
withImportBlock
|
|
1593
1587
|
};
|
package/dist/sync-memory.js
CHANGED
|
@@ -65,14 +65,6 @@ var SystemDoxApiClient = class {
|
|
|
65
65
|
this.config = config;
|
|
66
66
|
this.cache = new ResponseCache(config.cacheTtlMs);
|
|
67
67
|
}
|
|
68
|
-
async validate() {
|
|
69
|
-
const res = await this.fetchRaw("/v1/health");
|
|
70
|
-
if (!res.ok) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
68
|
async get(path, params) {
|
|
77
69
|
const url = this.buildUrl(path, params);
|
|
78
70
|
const cacheKey = url.toString();
|
|
@@ -86,8 +78,8 @@ var SystemDoxApiClient = class {
|
|
|
86
78
|
}
|
|
87
79
|
/** A read that skips the cache: an install must take the version that is
|
|
88
80
|
* latest now, not the one cached before someone published another. */
|
|
89
|
-
async getFresh(path) {
|
|
90
|
-
const res = await this.fetchRaw(path);
|
|
81
|
+
async getFresh(path, signal) {
|
|
82
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
91
83
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
92
84
|
return await res.json();
|
|
93
85
|
}
|
|
@@ -117,8 +109,8 @@ var SystemDoxApiClient = class {
|
|
|
117
109
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
118
110
|
// plugin-path act on what the workspace holds now.
|
|
119
111
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
120
|
-
getOwnTeamSetup() {
|
|
121
|
-
return this.getFresh(TEAM_SETUP);
|
|
112
|
+
getOwnTeamSetup(signal) {
|
|
113
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
122
114
|
}
|
|
123
115
|
getTeamSetup(workspace) {
|
|
124
116
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -222,14 +214,15 @@ var SystemDoxApiClient = class {
|
|
|
222
214
|
}
|
|
223
215
|
return url;
|
|
224
216
|
}
|
|
225
|
-
async fetchRaw(path, params) {
|
|
217
|
+
async fetchRaw(path, params, signal) {
|
|
226
218
|
const url = this.buildUrl(path, params);
|
|
227
219
|
return fetch(url, {
|
|
228
220
|
headers: {
|
|
229
221
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
230
222
|
"X-API-Key": this.config.apiKey,
|
|
231
223
|
Accept: "application/json"
|
|
232
|
-
}
|
|
224
|
+
},
|
|
225
|
+
signal
|
|
233
226
|
});
|
|
234
227
|
}
|
|
235
228
|
};
|
|
@@ -650,6 +643,27 @@ async function readCredentials() {
|
|
|
650
643
|
return readJson(credentialsPath(), {});
|
|
651
644
|
}
|
|
652
645
|
var run = promisify(execFile);
|
|
646
|
+
async function localApiKey(env = process.env) {
|
|
647
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
648
|
+
const keys = Object.entries(await readCredentials()).filter(
|
|
649
|
+
(e) => typeof e[1]?.apiKey === "string" && e[1].apiKey.length > 0
|
|
650
|
+
);
|
|
651
|
+
const wanted = env.SYSTEMDOX_WORKSPACE;
|
|
652
|
+
if (wanted) {
|
|
653
|
+
const key = keys.find(([ws]) => ws === wanted)?.[1].apiKey;
|
|
654
|
+
if (key) return key;
|
|
655
|
+
throw new Error(
|
|
656
|
+
`~/.claude/systemdox/credentials.json holds no key for ${wanted} (SYSTEMDOX_WORKSPACE). Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (keys.length === 1) return keys[0][1].apiKey;
|
|
660
|
+
if (keys.length > 1) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`~/.claude/systemdox/credentials.json holds keys for ${keys.map(([ws]) => ws).join(", ")}: set SYSTEMDOX_WORKSPACE to the workspace to use, or SYSTEMDOX_API_KEY to its key.`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
throw new Error("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.");
|
|
666
|
+
}
|
|
653
667
|
function apiUrlFrom(env = process.env) {
|
|
654
668
|
return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
|
|
655
669
|
}
|
|
@@ -1055,17 +1069,6 @@ async function upload(client, ws, project, plan, team, report) {
|
|
|
1055
1069
|
}
|
|
1056
1070
|
await send();
|
|
1057
1071
|
}
|
|
1058
|
-
async function keyFromEnvironment(env) {
|
|
1059
|
-
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
1060
|
-
const entries2 = Object.entries(await readCredentials()).filter(([, v]) => typeof v?.apiKey === "string" && v.apiKey);
|
|
1061
|
-
if (entries2.length === 1) return entries2[0][1].apiKey;
|
|
1062
|
-
if (entries2.length > 1) {
|
|
1063
|
-
throw new SyncFailed(
|
|
1064
|
-
`~/.claude/systemdox/credentials.json holds keys for ${entries2.map(([w]) => w).join(", ")}: set SYSTEMDOX_API_KEY to the key of the workspace to sync.`
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
throw new SyncFailed("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.");
|
|
1068
|
-
}
|
|
1069
1072
|
async function syncMemory(opts) {
|
|
1070
1073
|
const { client, out } = opts;
|
|
1071
1074
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
@@ -1201,7 +1204,7 @@ async function runSyncMemory(argv, io, env = process.env) {
|
|
|
1201
1204
|
args = parseSyncMemoryArgs(argv);
|
|
1202
1205
|
if (!client) {
|
|
1203
1206
|
const apiUrl = apiUrlFrom(env);
|
|
1204
|
-
const apiKey = await
|
|
1207
|
+
const apiKey = await localApiKey(env);
|
|
1205
1208
|
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1206
1209
|
}
|
|
1207
1210
|
} catch (e) {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { SystemDoxApiClient } from "../lib/api-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* The document graph over MCP (ADR-224, specs/changes/document-graph/contract.md §5).
|
|
5
|
+
* Three read-only tools on the hosted and the local server. Every answer says
|
|
6
|
+
* how each link is known, and one that was cut says so.
|
|
7
|
+
*/
|
|
8
|
+
interface GraphNode {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
type: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
interface NeighbourLink {
|
|
16
|
+
direction: "out" | "in";
|
|
17
|
+
relation: string;
|
|
18
|
+
provenance: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
file: string;
|
|
21
|
+
line: number;
|
|
22
|
+
other: GraphNode;
|
|
23
|
+
}
|
|
24
|
+
interface AmbiguousLink {
|
|
25
|
+
reference: string;
|
|
26
|
+
file: string;
|
|
27
|
+
line: number;
|
|
28
|
+
candidates: GraphNode[];
|
|
29
|
+
}
|
|
30
|
+
interface Neighbours {
|
|
31
|
+
doc: GraphNode;
|
|
32
|
+
links: NeighbourLink[];
|
|
33
|
+
ambiguous: AmbiguousLink[];
|
|
34
|
+
total: number;
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface PathStep {
|
|
38
|
+
from: GraphNode;
|
|
39
|
+
to: GraphNode;
|
|
40
|
+
relation: string;
|
|
41
|
+
provenance: string;
|
|
42
|
+
confidence: number;
|
|
43
|
+
file: string;
|
|
44
|
+
line: number;
|
|
45
|
+
reversed: boolean;
|
|
46
|
+
}
|
|
47
|
+
interface GraphPath {
|
|
48
|
+
found: boolean;
|
|
49
|
+
hops: number;
|
|
50
|
+
confidence: number;
|
|
51
|
+
steps: PathStep[];
|
|
52
|
+
}
|
|
53
|
+
interface RepoFreshness {
|
|
54
|
+
repo: string;
|
|
55
|
+
documents: number;
|
|
56
|
+
links: number;
|
|
57
|
+
indexedCommit: string;
|
|
58
|
+
indexedAt: string;
|
|
59
|
+
headCommit: string;
|
|
60
|
+
pushedAt: string;
|
|
61
|
+
behind: boolean;
|
|
62
|
+
}
|
|
63
|
+
interface GraphStats {
|
|
64
|
+
nodeCount: number;
|
|
65
|
+
edgeCount: number;
|
|
66
|
+
links: {
|
|
67
|
+
declared: number;
|
|
68
|
+
extracted: number;
|
|
69
|
+
inferred: number;
|
|
70
|
+
ambiguous: number;
|
|
71
|
+
};
|
|
72
|
+
repos: RepoFreshness[];
|
|
73
|
+
}
|
|
74
|
+
interface RepoLink {
|
|
75
|
+
source: string;
|
|
76
|
+
target: string;
|
|
77
|
+
relation: string;
|
|
78
|
+
provenance: string;
|
|
79
|
+
confidence: number;
|
|
80
|
+
file: string;
|
|
81
|
+
line: number;
|
|
82
|
+
}
|
|
83
|
+
export declare function formatNeighbours(answer: Neighbours): string;
|
|
84
|
+
export declare function formatPath(answer: GraphPath, maxHops: number): string;
|
|
85
|
+
export declare function formatStats(stats: GraphStats, repoLinks: RepoLink[]): string;
|
|
86
|
+
export declare function registerDocumentGraph(server: McpServer, apiClient: SystemDoxApiClient): void;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { SystemDoxApiClient } from "../lib/api-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* get_change (ADR-218, specs/changes/change-page/contract.md §5.1): one change
|
|
5
|
+
* as SystemDox last synced it, in one read of GET /v1/changes/{owner}/{repo}/{slug}
|
|
6
|
+
* (openapi.yaml, operationId getChange). The answer is Markdown — the title,
|
|
7
|
+
* key and size; the parts with their states and pull requests; the decision;
|
|
8
|
+
* the contract files; every feature file with its scenarios as
|
|
9
|
+
* `id · state · title`; the plan; the release pull requests — then the Change
|
|
10
|
+
* object whole in a fenced JSON block, and last the brief's verdict. A build
|
|
11
|
+
* session calls it first and stops when the brief is not passed (the
|
|
12
|
+
* *Implement story* prompt template's first step).
|
|
13
|
+
*/
|
|
14
|
+
/** ADR-218 §2.3.2's five words, read from GitHub, never written by SystemDox. */
|
|
15
|
+
export type State = "not yet" | "waiting for your review" | "passed" | "being built" | "done";
|
|
16
|
+
export interface PullRequest {
|
|
17
|
+
number: number;
|
|
18
|
+
title: string;
|
|
19
|
+
state: "open" | "merged";
|
|
20
|
+
base: string;
|
|
21
|
+
repo: string;
|
|
22
|
+
openedAt: string;
|
|
23
|
+
mergedAt: string | null;
|
|
24
|
+
url: string;
|
|
25
|
+
approvingReview: {
|
|
26
|
+
by: string;
|
|
27
|
+
at: string;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
export interface TrackedFile {
|
|
31
|
+
path: string;
|
|
32
|
+
docId: string;
|
|
33
|
+
pullRequest: PullRequest | null;
|
|
34
|
+
/** The file's last commit belongs to no pull request; it counts as merged. */
|
|
35
|
+
direct: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ScenarioCounts {
|
|
38
|
+
proposed: number;
|
|
39
|
+
built: number;
|
|
40
|
+
defect: number;
|
|
41
|
+
unbuilt: number;
|
|
42
|
+
open: number;
|
|
43
|
+
untagged: number;
|
|
44
|
+
total: number;
|
|
45
|
+
}
|
|
46
|
+
export interface Scenario {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
/** The state tag without `@`; empty when untagged. */
|
|
50
|
+
state: "proposed" | "built" | "defect" | "unbuilt" | "open" | "";
|
|
51
|
+
steps: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface FeatureFile extends TrackedFile {
|
|
54
|
+
title: string;
|
|
55
|
+
runner: "pytest-bdd" | "vitest" | "node:test" | "";
|
|
56
|
+
scenarios: Scenario[];
|
|
57
|
+
counts: ScenarioCounts;
|
|
58
|
+
}
|
|
59
|
+
export interface Board {
|
|
60
|
+
number: number;
|
|
61
|
+
title: string;
|
|
62
|
+
url: string;
|
|
63
|
+
items: {
|
|
64
|
+
todo: number;
|
|
65
|
+
inProgress: number;
|
|
66
|
+
done: number;
|
|
67
|
+
truncated: boolean;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export interface Change {
|
|
71
|
+
key: string;
|
|
72
|
+
owner: string;
|
|
73
|
+
repo: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
title: string;
|
|
76
|
+
size: "small" | "ordinary" | "foundational";
|
|
77
|
+
archived: boolean;
|
|
78
|
+
/** `decision` is null when the change has no decision (a small change). */
|
|
79
|
+
states: {
|
|
80
|
+
decision: State | null;
|
|
81
|
+
contract: State;
|
|
82
|
+
behaviour: State;
|
|
83
|
+
plan: State;
|
|
84
|
+
release: State;
|
|
85
|
+
};
|
|
86
|
+
counts: ScenarioCounts;
|
|
87
|
+
waitingForReview: PullRequest[];
|
|
88
|
+
lastActivity: string;
|
|
89
|
+
syncedAt: string;
|
|
90
|
+
proposal: TrackedFile;
|
|
91
|
+
decision: {
|
|
92
|
+
adr: string;
|
|
93
|
+
title: string;
|
|
94
|
+
status: string;
|
|
95
|
+
docId: string;
|
|
96
|
+
pullRequest: PullRequest | null;
|
|
97
|
+
} | null;
|
|
98
|
+
contract: {
|
|
99
|
+
files: TrackedFile[];
|
|
100
|
+
operations: {
|
|
101
|
+
method: string;
|
|
102
|
+
path: string;
|
|
103
|
+
operationId: string;
|
|
104
|
+
summary: string;
|
|
105
|
+
}[];
|
|
106
|
+
channels: {
|
|
107
|
+
name: string;
|
|
108
|
+
address: string;
|
|
109
|
+
messages: string[];
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
behaviour: {
|
|
113
|
+
files: FeatureFile[];
|
|
114
|
+
counts: ScenarioCounts;
|
|
115
|
+
};
|
|
116
|
+
plan: {
|
|
117
|
+
file: TrackedFile | null;
|
|
118
|
+
draftId: string | null;
|
|
119
|
+
board: Board | null;
|
|
120
|
+
};
|
|
121
|
+
release: {
|
|
122
|
+
prs: PullRequest[];
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The answer's last line (contract.md §5.1): "The brief is passed", or "The
|
|
127
|
+
* brief is not passed: <part> is <state>", naming each part that waits in the
|
|
128
|
+
* page's order. A change without a decision (`states.decision` null) is not
|
|
129
|
+
* held up by one.
|
|
130
|
+
*/
|
|
131
|
+
export declare function briefVerdict(states: Change["states"]): string;
|
|
132
|
+
/** The answer of contract.md §5.1, its verdict on the last line. */
|
|
133
|
+
export declare function formatChange(change: Change): string;
|
|
134
|
+
export declare function registerGetChange(server: McpServer, apiClient: Pick<SystemDoxApiClient, "getFresh">): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/tools/get-change.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
|
|
6
|
+
var REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
|
|
7
|
+
var SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
8
|
+
var SLUG_MAX = 64;
|
|
9
|
+
var PARTS = ["decision", "contract", "behaviour", "plan", "release"];
|
|
10
|
+
var BRIEF = ["decision", "contract", "behaviour", "plan"];
|
|
11
|
+
var PAST_REVIEW = /* @__PURE__ */ new Set(["passed", "being built", "done"]);
|
|
12
|
+
function briefVerdict(states) {
|
|
13
|
+
const waiting = BRIEF.filter(
|
|
14
|
+
(part) => !(part === "decision" && states.decision === null) && !PAST_REVIEW.has(String(states[part]))
|
|
15
|
+
).map((part) => `${part} is ${states[part] ?? "unknown"}`);
|
|
16
|
+
return waiting.length === 0 ? "The brief is passed" : `The brief is not passed: ${waiting.join(", ")}`;
|
|
17
|
+
}
|
|
18
|
+
function prLink(pr) {
|
|
19
|
+
return `[${pr.repo}#${pr.number}](${pr.url}) ${pr.state}`;
|
|
20
|
+
}
|
|
21
|
+
function fileSource(file) {
|
|
22
|
+
if (file.pullRequest) return prLink(file.pullRequest);
|
|
23
|
+
return file.direct ? "committed directly" : "\u2014";
|
|
24
|
+
}
|
|
25
|
+
function partPullRequests(change, part) {
|
|
26
|
+
const files = part === "contract" ? change.contract.files : part === "behaviour" ? change.behaviour.files : part === "plan" ? [change.plan.file] : [];
|
|
27
|
+
const prs = part === "decision" ? [change.decision?.pullRequest ?? null] : part === "release" ? change.release.prs : files.map((file) => file?.pullRequest ?? null);
|
|
28
|
+
const unique = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const pr of prs) {
|
|
30
|
+
if (pr && !unique.has(`${pr.repo}#${pr.number}`)) unique.set(`${pr.repo}#${pr.number}`, pr);
|
|
31
|
+
}
|
|
32
|
+
return [...unique.values()].sort((a, b) => Number(b.state === "open") - Number(a.state === "open"));
|
|
33
|
+
}
|
|
34
|
+
function countsLine(counts) {
|
|
35
|
+
const tags = ["proposed", "built", "defect", "unbuilt", "open", "untagged"].filter((tag) => counts[tag] > 0).map((tag) => `${counts[tag]} ${tag}`);
|
|
36
|
+
return `${counts.total} scenario(s)` + (tags.length > 0 ? `: ${tags.join(", ")}` : "");
|
|
37
|
+
}
|
|
38
|
+
function partsTable(change) {
|
|
39
|
+
const rows = PARTS.filter((part) => !(part === "decision" && change.states.decision === null)).map((part) => {
|
|
40
|
+
const prs = partPullRequests(change, part);
|
|
41
|
+
return `| ${part} | ${change.states[part]} | ${prs.length > 0 ? prs.map(prLink).join(", ") : "\u2014"} |`;
|
|
42
|
+
});
|
|
43
|
+
return ["| Part | State | Pull request |", "|---|---|---|", ...rows].join("\n");
|
|
44
|
+
}
|
|
45
|
+
function decisionSection(change) {
|
|
46
|
+
const decision = change.decision;
|
|
47
|
+
if (decision === null) return "## Decision\n\nNone: the proposal names no ADR (a small change).";
|
|
48
|
+
const status = decision.status || "no record in a connected repository";
|
|
49
|
+
return `## Decision
|
|
50
|
+
|
|
51
|
+
${decision.adr} \xB7 ${status}${decision.title ? ` \u2014 ${decision.title}` : ""}`;
|
|
52
|
+
}
|
|
53
|
+
function contractSection(change) {
|
|
54
|
+
const files = change.contract.files;
|
|
55
|
+
return "## Contract\n\n" + (files.length > 0 ? files.map((file) => `- ${file.path} \u2014 ${fileSource(file)}`).join("\n") : "No contract file yet (openapi.yaml, asyncapi.yaml or contract.md).");
|
|
56
|
+
}
|
|
57
|
+
function behaviourSection(change) {
|
|
58
|
+
const { files, counts } = change.behaviour;
|
|
59
|
+
if (files.length === 0) {
|
|
60
|
+
return `## Behaviour
|
|
61
|
+
|
|
62
|
+
No feature file names this change. A feature file joins it when its path or its first ten lines name \`${change.slug}\`${change.decision ? ` or ${change.decision.adr}` : ""}.`;
|
|
63
|
+
}
|
|
64
|
+
const blocks = files.map(
|
|
65
|
+
(file) => [
|
|
66
|
+
`### ${file.path}`,
|
|
67
|
+
"",
|
|
68
|
+
[file.title, file.runner ? `runner ${file.runner}` : "", fileSource(file), countsLine(file.counts)].filter(Boolean).join(" \xB7 "),
|
|
69
|
+
"",
|
|
70
|
+
...file.scenarios.map((s) => `- ${s.id || "(no id)"} \xB7 ${s.state || "untagged"} \xB7 ${s.title}`)
|
|
71
|
+
].join("\n")
|
|
72
|
+
);
|
|
73
|
+
return ["## Behaviour", countsLine(counts), ...blocks].join("\n\n");
|
|
74
|
+
}
|
|
75
|
+
function planSection(change) {
|
|
76
|
+
const { file, draftId, board } = change.plan;
|
|
77
|
+
const boardLine = board ? `[#${board.number} ${board.title}](${board.url}) \u2014 todo ${board.items.todo} \xB7 in progress ${board.items.inProgress} \xB7 done ${board.items.done}` + (board.items.truncated ? " (counted over its first 500 items)" : "") : "none";
|
|
78
|
+
return [
|
|
79
|
+
"## Plan",
|
|
80
|
+
"",
|
|
81
|
+
`- plan.md: ${file ? `${file.path} \u2014 ${fileSource(file)}` : "none"}`,
|
|
82
|
+
`- Draft: ${draftId ?? "none"}`,
|
|
83
|
+
`- Board: ${boardLine}`
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
function releaseSection(change) {
|
|
87
|
+
const lines = change.release.prs.map(
|
|
88
|
+
(pr) => `- ${prLink(pr)} \xB7 ${pr.title} \xB7 ` + (pr.approvingReview ? `approved by ${pr.approvingReview.by} at ${pr.approvingReview.at}` : "no approving review on its head yet")
|
|
89
|
+
);
|
|
90
|
+
return `## Release
|
|
91
|
+
|
|
92
|
+
${lines.length > 0 ? lines.join("\n") : "No release pull request is open."}`;
|
|
93
|
+
}
|
|
94
|
+
function formatChange(change) {
|
|
95
|
+
const header = `# ${change.title}
|
|
96
|
+
|
|
97
|
+
\`${change.key}\` \xB7 size: ${change.size}` + (change.archived ? " \xB7 archived: read-only, done" : "") + ` \xB7 synced ${change.syncedAt}`;
|
|
98
|
+
return [
|
|
99
|
+
header,
|
|
100
|
+
partsTable(change),
|
|
101
|
+
decisionSection(change),
|
|
102
|
+
contractSection(change),
|
|
103
|
+
behaviourSection(change),
|
|
104
|
+
planSection(change),
|
|
105
|
+
releaseSection(change),
|
|
106
|
+
// Compact: a change's every scenario with its steps rides in this block.
|
|
107
|
+
"```json\n" + JSON.stringify(change) + "\n```",
|
|
108
|
+
briefVerdict(change.states)
|
|
109
|
+
].join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
function registerGetChange(server, apiClient) {
|
|
112
|
+
server.registerTool(
|
|
113
|
+
"get_change",
|
|
114
|
+
{
|
|
115
|
+
title: "Get Change",
|
|
116
|
+
description: `Read one change (a specs/changes/<slug>/ folder of a connected repository) as SystemDox last synced it from GitHub: the state of each part of its brief (decision, contract, behaviour, plan) and of its release, each with its pull request; the decision's ADR and status; the contract files; every feature file that names the change, with its scenarios as id \xB7 state \xB7 title; the plan (plan.md, the draft, the board); the release pull requests; and the Change object as JSON. The last line says "The brief is passed" or "The brief is not passed: <part> is <state>". Call it before building a story, and stop if the brief is not passed or the story's scenarios are not proposed. Read-only.`,
|
|
117
|
+
inputSchema: {
|
|
118
|
+
repo: z.string().regex(REPO_RE, "the repository as owner/name, e.g. acme/booking-api").describe("The repository that holds the change, as owner/name"),
|
|
119
|
+
slug: z.string().max(SLUG_MAX).regex(SLUG_RE, "a change folder's name: lower-case words joined by -, e.g. customer-reminders").describe("The change's folder name: specs/changes/<slug>/")
|
|
120
|
+
},
|
|
121
|
+
annotations: { readOnlyHint: true }
|
|
122
|
+
},
|
|
123
|
+
async ({ repo, slug }) => {
|
|
124
|
+
const [owner, name] = repo.split("/");
|
|
125
|
+
let change;
|
|
126
|
+
try {
|
|
127
|
+
change = await apiClient.getFresh(
|
|
128
|
+
`/v1/changes/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`
|
|
129
|
+
);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err.status === 404) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: "text", text: `No such change ${repo}/${slug}` }],
|
|
134
|
+
isError: true
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
return { content: [{ type: "text", text: formatChange(change) }] };
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
briefVerdict,
|
|
145
|
+
formatChange,
|
|
146
|
+
registerGetChange
|
|
147
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { SystemDoxApiClient } from "../lib/api-client.js";
|
|
3
|
+
export declare function registerGetTeamSetup(server: McpServer, apiClient: SystemDoxApiClient, { localFiles, keyProblem }: {
|
|
4
|
+
localFiles: boolean;
|
|
5
|
+
keyProblem?: string;
|
|
6
|
+
}): void;
|
package/dist/tools/skills.js
CHANGED
|
@@ -29,6 +29,7 @@ import { homedir } from "os";
|
|
|
29
29
|
import { dirname, join, relative, resolve, sep } from "path";
|
|
30
30
|
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
31
31
|
var ENTRY_FILE = "SKILL.md";
|
|
32
|
+
var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
32
33
|
var MAX_FILES = 100;
|
|
33
34
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
34
35
|
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
@@ -53,7 +54,7 @@ function isValidName(name) {
|
|
|
53
54
|
return name.length <= 64 && NAME_RE.test(name);
|
|
54
55
|
}
|
|
55
56
|
function isValidPath(path) {
|
|
56
|
-
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
57
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
57
58
|
}
|
|
58
59
|
function frontmatterName(skillMd) {
|
|
59
60
|
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
@@ -291,7 +292,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
291
292
|
const { skills } = await apiClient.get(SKILLS);
|
|
292
293
|
if (!skills.length) {
|
|
293
294
|
return text(
|
|
294
|
-
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient))}`
|
|
295
|
+
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/team-setup/skills", await workspaceBase(apiClient))}`
|
|
295
296
|
);
|
|
296
297
|
}
|
|
297
298
|
return text(
|
package/dist/tools/team-setup.js
CHANGED
|
@@ -14,13 +14,14 @@ import { existsSync } from "fs";
|
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { dirname, join, relative, resolve, sep } from "path";
|
|
16
16
|
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
17
|
+
var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
17
18
|
var MAX_FILES = 100;
|
|
18
19
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
19
20
|
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
20
21
|
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
21
22
|
var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
22
23
|
function isValidPath(path) {
|
|
23
|
-
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
24
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
24
25
|
}
|
|
25
26
|
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
26
27
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemdox/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"mcpName": "io.github.puglieseweb/systemdox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Give AI coding agents your architecture context over MCP — checks, specs, ADRs, project rules, and docs from SystemDox, delivered at write time",
|