@systemdox/mcp-server 0.1.55 → 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/README.md +25 -5
- package/dist/cli.js +80 -13
- package/dist/http.js +1825 -1752
- package/dist/index.js +79 -12
- package/dist/install-skill.js +28 -3
- package/dist/lib/api-client.d.ts +4 -0
- package/dist/lib/api-client.js +28 -3
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/mcp-answer.d.ts +18 -0
- package/dist/plugin-path.js +28 -3
- package/dist/server.d.ts +5 -1
- package/dist/server.js +51 -9
- package/dist/setup-mcp.js +28 -3
- package/dist/setup-team.js +28 -3
- package/dist/sync-memory.js +28 -3
- package/package.json +10 -1
package/dist/index.js
CHANGED
|
@@ -54,6 +54,17 @@ var SystemDoxApiError = class extends Error {
|
|
|
54
54
|
code;
|
|
55
55
|
body;
|
|
56
56
|
};
|
|
57
|
+
function unescapedPath(path) {
|
|
58
|
+
let current = path.split(/[?#]/, 1)[0];
|
|
59
|
+
for (; ; ) {
|
|
60
|
+
const next = current.replace(/[\t\n\r]/g, "").replace(/%(?:2e|2f|5c|25|09|0a|0d)/gi, (escape) => decodeURIComponent(escape));
|
|
61
|
+
if (next === current) return current;
|
|
62
|
+
current = next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function walksThePath(path) {
|
|
66
|
+
return unescapedPath(path).split(/[/\\]/).some((segment) => segment === "." || segment === "..");
|
|
67
|
+
}
|
|
57
68
|
var TEAM_SETUP = "/v1/team-setup";
|
|
58
69
|
var seg = (s) => encodeURIComponent(String(s));
|
|
59
70
|
var SystemDoxApiClient = class {
|
|
@@ -89,7 +100,7 @@ var SystemDoxApiClient = class {
|
|
|
89
100
|
}
|
|
90
101
|
async mutate(method, path, body) {
|
|
91
102
|
const url = this.buildUrl(path);
|
|
92
|
-
const res = await
|
|
103
|
+
const res = await this.call(url, {
|
|
93
104
|
method,
|
|
94
105
|
headers: {
|
|
95
106
|
// The API Gateway authorizer only reads the Authorization header;
|
|
@@ -139,7 +150,7 @@ var SystemDoxApiClient = class {
|
|
|
139
150
|
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
140
151
|
* header. The caller checks the BYTES against the hash it was given. */
|
|
141
152
|
async getPluginZip(workspace, version) {
|
|
142
|
-
const res = await
|
|
153
|
+
const res = await this.call(
|
|
143
154
|
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
144
155
|
{
|
|
145
156
|
headers: {
|
|
@@ -205,6 +216,10 @@ var SystemDoxApiClient = class {
|
|
|
205
216
|
buildUrl(path, params) {
|
|
206
217
|
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
207
218
|
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
219
|
+
const { origin, pathname } = new URL(base);
|
|
220
|
+
if (walksThePath(path) || url.origin !== origin || !url.pathname.startsWith(`${pathname.replace(/\/+$/, "")}/`)) {
|
|
221
|
+
throw new Error("Refused: this id leaves the API's own path.");
|
|
222
|
+
}
|
|
208
223
|
if (params) {
|
|
209
224
|
for (const [key, value] of Object.entries(params)) {
|
|
210
225
|
url.searchParams.set(key, value);
|
|
@@ -214,7 +229,7 @@ var SystemDoxApiClient = class {
|
|
|
214
229
|
}
|
|
215
230
|
async fetchRaw(path, params, signal) {
|
|
216
231
|
const url = this.buildUrl(path, params);
|
|
217
|
-
return
|
|
232
|
+
return this.call(url, {
|
|
218
233
|
headers: {
|
|
219
234
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
220
235
|
"X-API-Key": this.config.apiKey,
|
|
@@ -223,6 +238,16 @@ var SystemDoxApiClient = class {
|
|
|
223
238
|
signal
|
|
224
239
|
});
|
|
225
240
|
}
|
|
241
|
+
/** Every call of this client, so `onAnswer` hears each answer. */
|
|
242
|
+
async call(url, init) {
|
|
243
|
+
const res = await fetch(url, init);
|
|
244
|
+
const { onAnswer } = this.config;
|
|
245
|
+
if (onAnswer) {
|
|
246
|
+
const refused2 = res.ok ? void 0 : new SystemDoxApiError(res.status, await res.clone().text());
|
|
247
|
+
onAnswer({ status: res.status, code: refused2?.code });
|
|
248
|
+
}
|
|
249
|
+
return res;
|
|
250
|
+
}
|
|
226
251
|
};
|
|
227
252
|
|
|
228
253
|
// src/lib/config.ts
|
|
@@ -896,7 +921,7 @@ function registerGetSpecs(server, apiClient2) {
|
|
|
896
921
|
async ({ draft_id, status, search }) => {
|
|
897
922
|
if (draft_id) {
|
|
898
923
|
const spec = await apiClient2.get(
|
|
899
|
-
`/v1/specs/${draft_id}`
|
|
924
|
+
`/v1/specs/${encodeURIComponent(draft_id)}`
|
|
900
925
|
);
|
|
901
926
|
const sections = [`# ${spec.name}
|
|
902
927
|
|
|
@@ -2546,7 +2571,8 @@ function registerUpdateSpec(server, apiClient2) {
|
|
|
2546
2571
|
}
|
|
2547
2572
|
},
|
|
2548
2573
|
async ({ draft_id, section, content, requirement_name }) => {
|
|
2549
|
-
const
|
|
2574
|
+
const specPath = `/v1/specs/${encodeURIComponent(draft_id)}`;
|
|
2575
|
+
const spec = await apiClient2.get(specPath);
|
|
2550
2576
|
if (spec.status !== "draft") {
|
|
2551
2577
|
return {
|
|
2552
2578
|
content: [
|
|
@@ -2571,8 +2597,8 @@ function registerUpdateSpec(server, apiClient2) {
|
|
|
2571
2597
|
};
|
|
2572
2598
|
body.requirements = [...existing, newReq];
|
|
2573
2599
|
}
|
|
2574
|
-
await apiClient2.put(
|
|
2575
|
-
apiClient2.invalidateCache(
|
|
2600
|
+
await apiClient2.put(specPath, body);
|
|
2601
|
+
apiClient2.invalidateCache(specPath);
|
|
2576
2602
|
apiClient2.invalidateCache("/v1/specs");
|
|
2577
2603
|
return {
|
|
2578
2604
|
content: [
|
|
@@ -2631,7 +2657,7 @@ ${decision}
|
|
|
2631
2657
|
## Consequences
|
|
2632
2658
|
|
|
2633
2659
|
${consequences}` + relatedSection;
|
|
2634
|
-
await apiClient2.put(`/v1/specs/${created.draftId}`, {
|
|
2660
|
+
await apiClient2.put(`/v1/specs/${encodeURIComponent(created.draftId)}`, {
|
|
2635
2661
|
proposalContent: adrContent
|
|
2636
2662
|
});
|
|
2637
2663
|
apiClient2.invalidateCache("/v1/specs");
|
|
@@ -4013,15 +4039,56 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
4013
4039
|
}
|
|
4014
4040
|
|
|
4015
4041
|
// src/server.ts
|
|
4016
|
-
var SERVER_VERSION = true ? "0.1.
|
|
4017
|
-
|
|
4018
|
-
|
|
4042
|
+
var SERVER_VERSION = true ? "0.1.56" : "0.0.0-dev";
|
|
4043
|
+
var READ_TOOLS = [
|
|
4044
|
+
"get_context",
|
|
4045
|
+
"get_checks",
|
|
4046
|
+
"get_guardrails",
|
|
4047
|
+
"get_project_rules",
|
|
4048
|
+
"get_prompt_templates",
|
|
4049
|
+
"get_specs",
|
|
4050
|
+
"get_docs",
|
|
4051
|
+
"get_tech_stack",
|
|
4052
|
+
"get_planning_context",
|
|
4053
|
+
"get_plan_board",
|
|
4054
|
+
"get_change",
|
|
4055
|
+
"get_team_setup",
|
|
4056
|
+
"get_document_links",
|
|
4057
|
+
"trace_document_path",
|
|
4058
|
+
"get_graph_stats",
|
|
4059
|
+
"list_skills",
|
|
4060
|
+
"install_skill",
|
|
4061
|
+
"resolve_person"
|
|
4062
|
+
];
|
|
4063
|
+
var WRITE_TOOLS = [
|
|
4064
|
+
"create_check",
|
|
4065
|
+
"create_guardrail",
|
|
4066
|
+
"create_project_rule",
|
|
4067
|
+
"create_prompt_template",
|
|
4068
|
+
"update_spec",
|
|
4069
|
+
"report_decision",
|
|
4070
|
+
"publish_skill",
|
|
4071
|
+
"publish_setup_part"
|
|
4072
|
+
];
|
|
4073
|
+
function offering(server, access) {
|
|
4074
|
+
const offered = new Set(access === "write" ? [...READ_TOOLS, ...WRITE_TOOLS] : READ_TOOLS);
|
|
4075
|
+
return new Proxy(server, {
|
|
4076
|
+
get(target, property) {
|
|
4077
|
+
const value = Reflect.get(target, property, target);
|
|
4078
|
+
if (property !== "registerTool") return typeof value === "function" ? value.bind(target) : value;
|
|
4079
|
+
return (name, ...rest) => offered.has(name) ? target.registerTool(name, ...rest) : void 0;
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2, access } = {}) {
|
|
4084
|
+
const mcpServer = new McpServer(
|
|
4019
4085
|
{
|
|
4020
4086
|
name: "systemdox-mcp-server",
|
|
4021
4087
|
version: SERVER_VERSION
|
|
4022
4088
|
},
|
|
4023
4089
|
instructions ? { instructions } : void 0
|
|
4024
4090
|
);
|
|
4091
|
+
const server = access ? offering(mcpServer, access) : mcpServer;
|
|
4025
4092
|
registerGetGuardrails(server, apiClient2);
|
|
4026
4093
|
registerGetSpecs(server, apiClient2);
|
|
4027
4094
|
registerGetDocs(server, apiClient2);
|
|
@@ -4041,7 +4108,7 @@ function createServer(apiClient2, { localFiles = false, instructions, keyProblem
|
|
|
4041
4108
|
registerCreatePromptTemplate(server, apiClient2);
|
|
4042
4109
|
registerSkillTools(server, apiClient2, { localFiles });
|
|
4043
4110
|
registerTeamSetupTools(server, apiClient2, { localFiles });
|
|
4044
|
-
return
|
|
4111
|
+
return mcpServer;
|
|
4045
4112
|
}
|
|
4046
4113
|
|
|
4047
4114
|
// src/index.ts
|
package/dist/install-skill.js
CHANGED
|
@@ -54,6 +54,17 @@ var SystemDoxApiError = class extends Error {
|
|
|
54
54
|
code;
|
|
55
55
|
body;
|
|
56
56
|
};
|
|
57
|
+
function unescapedPath(path) {
|
|
58
|
+
let current = path.split(/[?#]/, 1)[0];
|
|
59
|
+
for (; ; ) {
|
|
60
|
+
const next = current.replace(/[\t\n\r]/g, "").replace(/%(?:2e|2f|5c|25|09|0a|0d)/gi, (escape) => decodeURIComponent(escape));
|
|
61
|
+
if (next === current) return current;
|
|
62
|
+
current = next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function walksThePath(path) {
|
|
66
|
+
return unescapedPath(path).split(/[/\\]/).some((segment) => segment === "." || segment === "..");
|
|
67
|
+
}
|
|
57
68
|
var TEAM_SETUP = "/v1/team-setup";
|
|
58
69
|
var seg = (s) => encodeURIComponent(String(s));
|
|
59
70
|
var SystemDoxApiClient = class {
|
|
@@ -89,7 +100,7 @@ var SystemDoxApiClient = class {
|
|
|
89
100
|
}
|
|
90
101
|
async mutate(method, path, body) {
|
|
91
102
|
const url = this.buildUrl(path);
|
|
92
|
-
const res = await
|
|
103
|
+
const res = await this.call(url, {
|
|
93
104
|
method,
|
|
94
105
|
headers: {
|
|
95
106
|
// The API Gateway authorizer only reads the Authorization header;
|
|
@@ -139,7 +150,7 @@ var SystemDoxApiClient = class {
|
|
|
139
150
|
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
140
151
|
* header. The caller checks the BYTES against the hash it was given. */
|
|
141
152
|
async getPluginZip(workspace, version) {
|
|
142
|
-
const res = await
|
|
153
|
+
const res = await this.call(
|
|
143
154
|
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
144
155
|
{
|
|
145
156
|
headers: {
|
|
@@ -205,6 +216,10 @@ var SystemDoxApiClient = class {
|
|
|
205
216
|
buildUrl(path, params) {
|
|
206
217
|
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
207
218
|
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
219
|
+
const { origin, pathname } = new URL(base);
|
|
220
|
+
if (walksThePath(path) || url.origin !== origin || !url.pathname.startsWith(`${pathname.replace(/\/+$/, "")}/`)) {
|
|
221
|
+
throw new Error("Refused: this id leaves the API's own path.");
|
|
222
|
+
}
|
|
208
223
|
if (params) {
|
|
209
224
|
for (const [key, value] of Object.entries(params)) {
|
|
210
225
|
url.searchParams.set(key, value);
|
|
@@ -214,7 +229,7 @@ var SystemDoxApiClient = class {
|
|
|
214
229
|
}
|
|
215
230
|
async fetchRaw(path, params, signal) {
|
|
216
231
|
const url = this.buildUrl(path, params);
|
|
217
|
-
return
|
|
232
|
+
return this.call(url, {
|
|
218
233
|
headers: {
|
|
219
234
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
220
235
|
"X-API-Key": this.config.apiKey,
|
|
@@ -223,6 +238,16 @@ var SystemDoxApiClient = class {
|
|
|
223
238
|
signal
|
|
224
239
|
});
|
|
225
240
|
}
|
|
241
|
+
/** Every call of this client, so `onAnswer` hears each answer. */
|
|
242
|
+
async call(url, init) {
|
|
243
|
+
const res = await fetch(url, init);
|
|
244
|
+
const { onAnswer } = this.config;
|
|
245
|
+
if (onAnswer) {
|
|
246
|
+
const refused = res.ok ? void 0 : new SystemDoxApiError(res.status, await res.clone().text());
|
|
247
|
+
onAnswer({ status: res.status, code: refused?.code });
|
|
248
|
+
}
|
|
249
|
+
return res;
|
|
250
|
+
}
|
|
226
251
|
};
|
|
227
252
|
|
|
228
253
|
// src/lib/config.ts
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class SystemDoxApiError extends Error {
|
|
|
13
13
|
readonly body: unknown;
|
|
14
14
|
constructor(status: number, text: string);
|
|
15
15
|
}
|
|
16
|
+
/** One answer of the REST API, with the `code` of a refusal that named one. */
|
|
17
|
+
export type ApiAnswer = Parameters<NonNullable<ServerConfig["onAnswer"]>>[0];
|
|
16
18
|
export declare class SystemDoxApiClient {
|
|
17
19
|
private config;
|
|
18
20
|
private cache;
|
|
@@ -94,4 +96,6 @@ export declare class SystemDoxApiClient {
|
|
|
94
96
|
invalidateCache(path?: string): void;
|
|
95
97
|
private buildUrl;
|
|
96
98
|
private fetchRaw;
|
|
99
|
+
/** Every call of this client, so `onAnswer` hears each answer. */
|
|
100
|
+
private call;
|
|
97
101
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -51,6 +51,17 @@ var SystemDoxApiError = class extends Error {
|
|
|
51
51
|
code;
|
|
52
52
|
body;
|
|
53
53
|
};
|
|
54
|
+
function unescapedPath(path) {
|
|
55
|
+
let current = path.split(/[?#]/, 1)[0];
|
|
56
|
+
for (; ; ) {
|
|
57
|
+
const next = current.replace(/[\t\n\r]/g, "").replace(/%(?:2e|2f|5c|25|09|0a|0d)/gi, (escape) => decodeURIComponent(escape));
|
|
58
|
+
if (next === current) return current;
|
|
59
|
+
current = next;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function walksThePath(path) {
|
|
63
|
+
return unescapedPath(path).split(/[/\\]/).some((segment) => segment === "." || segment === "..");
|
|
64
|
+
}
|
|
54
65
|
var TEAM_SETUP = "/v1/team-setup";
|
|
55
66
|
var seg = (s) => encodeURIComponent(String(s));
|
|
56
67
|
var SystemDoxApiClient = class {
|
|
@@ -86,7 +97,7 @@ var SystemDoxApiClient = class {
|
|
|
86
97
|
}
|
|
87
98
|
async mutate(method, path, body) {
|
|
88
99
|
const url = this.buildUrl(path);
|
|
89
|
-
const res = await
|
|
100
|
+
const res = await this.call(url, {
|
|
90
101
|
method,
|
|
91
102
|
headers: {
|
|
92
103
|
// The API Gateway authorizer only reads the Authorization header;
|
|
@@ -136,7 +147,7 @@ var SystemDoxApiClient = class {
|
|
|
136
147
|
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
137
148
|
* header. The caller checks the BYTES against the hash it was given. */
|
|
138
149
|
async getPluginZip(workspace, version) {
|
|
139
|
-
const res = await
|
|
150
|
+
const res = await this.call(
|
|
140
151
|
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
141
152
|
{
|
|
142
153
|
headers: {
|
|
@@ -202,6 +213,10 @@ var SystemDoxApiClient = class {
|
|
|
202
213
|
buildUrl(path, params) {
|
|
203
214
|
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
204
215
|
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
216
|
+
const { origin, pathname } = new URL(base);
|
|
217
|
+
if (walksThePath(path) || url.origin !== origin || !url.pathname.startsWith(`${pathname.replace(/\/+$/, "")}/`)) {
|
|
218
|
+
throw new Error("Refused: this id leaves the API's own path.");
|
|
219
|
+
}
|
|
205
220
|
if (params) {
|
|
206
221
|
for (const [key, value] of Object.entries(params)) {
|
|
207
222
|
url.searchParams.set(key, value);
|
|
@@ -211,7 +226,7 @@ var SystemDoxApiClient = class {
|
|
|
211
226
|
}
|
|
212
227
|
async fetchRaw(path, params, signal) {
|
|
213
228
|
const url = this.buildUrl(path, params);
|
|
214
|
-
return
|
|
229
|
+
return this.call(url, {
|
|
215
230
|
headers: {
|
|
216
231
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
217
232
|
"X-API-Key": this.config.apiKey,
|
|
@@ -220,6 +235,16 @@ var SystemDoxApiClient = class {
|
|
|
220
235
|
signal
|
|
221
236
|
});
|
|
222
237
|
}
|
|
238
|
+
/** Every call of this client, so `onAnswer` hears each answer. */
|
|
239
|
+
async call(url, init) {
|
|
240
|
+
const res = await fetch(url, init);
|
|
241
|
+
const { onAnswer } = this.config;
|
|
242
|
+
if (onAnswer) {
|
|
243
|
+
const refused = res.ok ? void 0 : new SystemDoxApiError(res.status, await res.clone().text());
|
|
244
|
+
onAnswer({ status: res.status, code: refused?.code });
|
|
245
|
+
}
|
|
246
|
+
return res;
|
|
247
|
+
}
|
|
223
248
|
};
|
|
224
249
|
export {
|
|
225
250
|
SystemDoxApiClient,
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ export interface ServerConfig {
|
|
|
3
3
|
apiUrl: string;
|
|
4
4
|
cacheTtlMs: number;
|
|
5
5
|
defaultOrg: string | undefined;
|
|
6
|
+
/** Hears every answer of the REST API, because a tool turns a refusal into a
|
|
7
|
+
* tool error and session-brief.ts hides it (ADR-229). */
|
|
8
|
+
onAnswer?: (answer: {
|
|
9
|
+
status: number;
|
|
10
|
+
code: string | undefined;
|
|
11
|
+
}) => void;
|
|
6
12
|
}
|
|
7
13
|
export declare const API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
|
|
8
14
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One MCP request, answered by a fresh server over a web-standard transport (no
|
|
3
|
+
* sessions): what `POST /mcp` and `POST /connect` have in common. It fits Lambda's
|
|
4
|
+
* execution model, and each caller keeps its own way of proving who is asking.
|
|
5
|
+
*/
|
|
6
|
+
import { type ServerOptions } from "../server.js";
|
|
7
|
+
import type { SystemDoxApiClient } from "./api-client.js";
|
|
8
|
+
interface Answer {
|
|
9
|
+
statusCode: number;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
body: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function answerMcpRequest(apiClient: SystemDoxApiClient, request: {
|
|
14
|
+
url: string;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
rawBody: string;
|
|
17
|
+
}, options?: Pick<ServerOptions, "access">): Promise<Answer>;
|
|
18
|
+
export {};
|
package/dist/plugin-path.js
CHANGED
|
@@ -58,6 +58,17 @@ var SystemDoxApiError = class extends Error {
|
|
|
58
58
|
code;
|
|
59
59
|
body;
|
|
60
60
|
};
|
|
61
|
+
function unescapedPath(path) {
|
|
62
|
+
let current = path.split(/[?#]/, 1)[0];
|
|
63
|
+
for (; ; ) {
|
|
64
|
+
const next = current.replace(/[\t\n\r]/g, "").replace(/%(?:2e|2f|5c|25|09|0a|0d)/gi, (escape) => decodeURIComponent(escape));
|
|
65
|
+
if (next === current) return current;
|
|
66
|
+
current = next;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function walksThePath(path) {
|
|
70
|
+
return unescapedPath(path).split(/[/\\]/).some((segment) => segment === "." || segment === "..");
|
|
71
|
+
}
|
|
61
72
|
var TEAM_SETUP = "/v1/team-setup";
|
|
62
73
|
var seg = (s) => encodeURIComponent(String(s));
|
|
63
74
|
var SystemDoxApiClient = class {
|
|
@@ -93,7 +104,7 @@ var SystemDoxApiClient = class {
|
|
|
93
104
|
}
|
|
94
105
|
async mutate(method, path, body) {
|
|
95
106
|
const url = this.buildUrl(path);
|
|
96
|
-
const res = await
|
|
107
|
+
const res = await this.call(url, {
|
|
97
108
|
method,
|
|
98
109
|
headers: {
|
|
99
110
|
// The API Gateway authorizer only reads the Authorization header;
|
|
@@ -143,7 +154,7 @@ var SystemDoxApiClient = class {
|
|
|
143
154
|
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
144
155
|
* header. The caller checks the BYTES against the hash it was given. */
|
|
145
156
|
async getPluginZip(workspace, version) {
|
|
146
|
-
const res = await
|
|
157
|
+
const res = await this.call(
|
|
147
158
|
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
148
159
|
{
|
|
149
160
|
headers: {
|
|
@@ -209,6 +220,10 @@ var SystemDoxApiClient = class {
|
|
|
209
220
|
buildUrl(path, params) {
|
|
210
221
|
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
211
222
|
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
223
|
+
const { origin, pathname } = new URL(base);
|
|
224
|
+
if (walksThePath(path) || url.origin !== origin || !url.pathname.startsWith(`${pathname.replace(/\/+$/, "")}/`)) {
|
|
225
|
+
throw new Error("Refused: this id leaves the API's own path.");
|
|
226
|
+
}
|
|
212
227
|
if (params) {
|
|
213
228
|
for (const [key, value] of Object.entries(params)) {
|
|
214
229
|
url.searchParams.set(key, value);
|
|
@@ -218,7 +233,7 @@ var SystemDoxApiClient = class {
|
|
|
218
233
|
}
|
|
219
234
|
async fetchRaw(path, params, signal) {
|
|
220
235
|
const url = this.buildUrl(path, params);
|
|
221
|
-
return
|
|
236
|
+
return this.call(url, {
|
|
222
237
|
headers: {
|
|
223
238
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
224
239
|
"X-API-Key": this.config.apiKey,
|
|
@@ -227,6 +242,16 @@ var SystemDoxApiClient = class {
|
|
|
227
242
|
signal
|
|
228
243
|
});
|
|
229
244
|
}
|
|
245
|
+
/** Every call of this client, so `onAnswer` hears each answer. */
|
|
246
|
+
async call(url, init) {
|
|
247
|
+
const res = await fetch(url, init);
|
|
248
|
+
const { onAnswer } = this.config;
|
|
249
|
+
if (onAnswer) {
|
|
250
|
+
const refused = res.ok ? void 0 : new SystemDoxApiError(res.status, await res.clone().text());
|
|
251
|
+
onAnswer({ status: res.status, code: refused?.code });
|
|
252
|
+
}
|
|
253
|
+
return res;
|
|
254
|
+
}
|
|
230
255
|
};
|
|
231
256
|
|
|
232
257
|
// src/lib/team-setup-files.ts
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { SystemDoxApiClient } from "./lib/api-client.js";
|
|
3
|
+
export type ConnectionAccess = "read" | "write";
|
|
3
4
|
export interface ServerOptions {
|
|
4
5
|
/** True only on the user's own computer (stdio): the skill tools may read
|
|
5
6
|
* and write the skill folder there. The hosted server never touches a disk
|
|
@@ -9,10 +10,13 @@ export interface ServerOptions {
|
|
|
9
10
|
instructions?: string;
|
|
10
11
|
/** Why the local server has no key, when it has none. */
|
|
11
12
|
keyProblem?: string;
|
|
13
|
+
/** A connection's access (ADR-229): `tools/list` holds only these. Left out,
|
|
14
|
+
* every tool is offered, as on `/mcp` and the local server. */
|
|
15
|
+
access?: ConnectionAccess;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* Build a SystemDox MCP server with all tools registered against the given
|
|
15
19
|
* API client. Shared by the stdio entry point (index.ts) and the hosted
|
|
16
20
|
* Streamable HTTP entry point (http.ts).
|
|
17
21
|
*/
|
|
18
|
-
export declare function createServer(apiClient: SystemDoxApiClient, { localFiles, instructions, keyProblem }?: ServerOptions): McpServer;
|
|
22
|
+
export declare function createServer(apiClient: SystemDoxApiClient, { localFiles, instructions, keyProblem, access }?: ServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -214,7 +214,7 @@ function registerGetSpecs(server, apiClient) {
|
|
|
214
214
|
async ({ draft_id, status, search }) => {
|
|
215
215
|
if (draft_id) {
|
|
216
216
|
const spec = await apiClient.get(
|
|
217
|
-
`/v1/specs/${draft_id}`
|
|
217
|
+
`/v1/specs/${encodeURIComponent(draft_id)}`
|
|
218
218
|
);
|
|
219
219
|
const sections = [`# ${spec.name}
|
|
220
220
|
|
|
@@ -2184,7 +2184,8 @@ function registerUpdateSpec(server, apiClient) {
|
|
|
2184
2184
|
}
|
|
2185
2185
|
},
|
|
2186
2186
|
async ({ draft_id, section, content, requirement_name }) => {
|
|
2187
|
-
const
|
|
2187
|
+
const specPath = `/v1/specs/${encodeURIComponent(draft_id)}`;
|
|
2188
|
+
const spec = await apiClient.get(specPath);
|
|
2188
2189
|
if (spec.status !== "draft") {
|
|
2189
2190
|
return {
|
|
2190
2191
|
content: [
|
|
@@ -2209,8 +2210,8 @@ function registerUpdateSpec(server, apiClient) {
|
|
|
2209
2210
|
};
|
|
2210
2211
|
body.requirements = [...existing, newReq];
|
|
2211
2212
|
}
|
|
2212
|
-
await apiClient.put(
|
|
2213
|
-
apiClient.invalidateCache(
|
|
2213
|
+
await apiClient.put(specPath, body);
|
|
2214
|
+
apiClient.invalidateCache(specPath);
|
|
2214
2215
|
apiClient.invalidateCache("/v1/specs");
|
|
2215
2216
|
return {
|
|
2216
2217
|
content: [
|
|
@@ -2269,7 +2270,7 @@ ${decision}
|
|
|
2269
2270
|
## Consequences
|
|
2270
2271
|
|
|
2271
2272
|
${consequences}` + relatedSection;
|
|
2272
|
-
await apiClient.put(`/v1/specs/${created.draftId}`, {
|
|
2273
|
+
await apiClient.put(`/v1/specs/${encodeURIComponent(created.draftId)}`, {
|
|
2273
2274
|
proposalContent: adrContent
|
|
2274
2275
|
});
|
|
2275
2276
|
apiClient.invalidateCache("/v1/specs");
|
|
@@ -3651,15 +3652,56 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
|
3651
3652
|
}
|
|
3652
3653
|
|
|
3653
3654
|
// src/server.ts
|
|
3654
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3655
|
-
|
|
3656
|
-
|
|
3655
|
+
var SERVER_VERSION = true ? "0.1.56" : "0.0.0-dev";
|
|
3656
|
+
var READ_TOOLS = [
|
|
3657
|
+
"get_context",
|
|
3658
|
+
"get_checks",
|
|
3659
|
+
"get_guardrails",
|
|
3660
|
+
"get_project_rules",
|
|
3661
|
+
"get_prompt_templates",
|
|
3662
|
+
"get_specs",
|
|
3663
|
+
"get_docs",
|
|
3664
|
+
"get_tech_stack",
|
|
3665
|
+
"get_planning_context",
|
|
3666
|
+
"get_plan_board",
|
|
3667
|
+
"get_change",
|
|
3668
|
+
"get_team_setup",
|
|
3669
|
+
"get_document_links",
|
|
3670
|
+
"trace_document_path",
|
|
3671
|
+
"get_graph_stats",
|
|
3672
|
+
"list_skills",
|
|
3673
|
+
"install_skill",
|
|
3674
|
+
"resolve_person"
|
|
3675
|
+
];
|
|
3676
|
+
var WRITE_TOOLS = [
|
|
3677
|
+
"create_check",
|
|
3678
|
+
"create_guardrail",
|
|
3679
|
+
"create_project_rule",
|
|
3680
|
+
"create_prompt_template",
|
|
3681
|
+
"update_spec",
|
|
3682
|
+
"report_decision",
|
|
3683
|
+
"publish_skill",
|
|
3684
|
+
"publish_setup_part"
|
|
3685
|
+
];
|
|
3686
|
+
function offering(server, access) {
|
|
3687
|
+
const offered = new Set(access === "write" ? [...READ_TOOLS, ...WRITE_TOOLS] : READ_TOOLS);
|
|
3688
|
+
return new Proxy(server, {
|
|
3689
|
+
get(target, property) {
|
|
3690
|
+
const value = Reflect.get(target, property, target);
|
|
3691
|
+
if (property !== "registerTool") return typeof value === "function" ? value.bind(target) : value;
|
|
3692
|
+
return (name, ...rest) => offered.has(name) ? target.registerTool(name, ...rest) : void 0;
|
|
3693
|
+
}
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
function createServer(apiClient, { localFiles = false, instructions, keyProblem, access } = {}) {
|
|
3697
|
+
const mcpServer = new McpServer(
|
|
3657
3698
|
{
|
|
3658
3699
|
name: "systemdox-mcp-server",
|
|
3659
3700
|
version: SERVER_VERSION
|
|
3660
3701
|
},
|
|
3661
3702
|
instructions ? { instructions } : void 0
|
|
3662
3703
|
);
|
|
3704
|
+
const server = access ? offering(mcpServer, access) : mcpServer;
|
|
3663
3705
|
registerGetGuardrails(server, apiClient);
|
|
3664
3706
|
registerGetSpecs(server, apiClient);
|
|
3665
3707
|
registerGetDocs(server, apiClient);
|
|
@@ -3679,7 +3721,7 @@ function createServer(apiClient, { localFiles = false, instructions, keyProblem
|
|
|
3679
3721
|
registerCreatePromptTemplate(server, apiClient);
|
|
3680
3722
|
registerSkillTools(server, apiClient, { localFiles });
|
|
3681
3723
|
registerTeamSetupTools(server, apiClient, { localFiles });
|
|
3682
|
-
return
|
|
3724
|
+
return mcpServer;
|
|
3683
3725
|
}
|
|
3684
3726
|
export {
|
|
3685
3727
|
createServer
|
package/dist/setup-mcp.js
CHANGED
|
@@ -56,6 +56,17 @@ var SystemDoxApiError = class extends Error {
|
|
|
56
56
|
code;
|
|
57
57
|
body;
|
|
58
58
|
};
|
|
59
|
+
function unescapedPath(path) {
|
|
60
|
+
let current = path.split(/[?#]/, 1)[0];
|
|
61
|
+
for (; ; ) {
|
|
62
|
+
const next = current.replace(/[\t\n\r]/g, "").replace(/%(?:2e|2f|5c|25|09|0a|0d)/gi, (escape) => decodeURIComponent(escape));
|
|
63
|
+
if (next === current) return current;
|
|
64
|
+
current = next;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function walksThePath(path) {
|
|
68
|
+
return unescapedPath(path).split(/[/\\]/).some((segment) => segment === "." || segment === "..");
|
|
69
|
+
}
|
|
59
70
|
var TEAM_SETUP = "/v1/team-setup";
|
|
60
71
|
var seg = (s) => encodeURIComponent(String(s));
|
|
61
72
|
var SystemDoxApiClient = class {
|
|
@@ -91,7 +102,7 @@ var SystemDoxApiClient = class {
|
|
|
91
102
|
}
|
|
92
103
|
async mutate(method, path, body) {
|
|
93
104
|
const url = this.buildUrl(path);
|
|
94
|
-
const res = await
|
|
105
|
+
const res = await this.call(url, {
|
|
95
106
|
method,
|
|
96
107
|
headers: {
|
|
97
108
|
// The API Gateway authorizer only reads the Authorization header;
|
|
@@ -141,7 +152,7 @@ var SystemDoxApiClient = class {
|
|
|
141
152
|
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
142
153
|
* header. The caller checks the BYTES against the hash it was given. */
|
|
143
154
|
async getPluginZip(workspace, version) {
|
|
144
|
-
const res = await
|
|
155
|
+
const res = await this.call(
|
|
145
156
|
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
146
157
|
{
|
|
147
158
|
headers: {
|
|
@@ -207,6 +218,10 @@ var SystemDoxApiClient = class {
|
|
|
207
218
|
buildUrl(path, params) {
|
|
208
219
|
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
209
220
|
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
221
|
+
const { origin, pathname } = new URL(base);
|
|
222
|
+
if (walksThePath(path) || url.origin !== origin || !url.pathname.startsWith(`${pathname.replace(/\/+$/, "")}/`)) {
|
|
223
|
+
throw new Error("Refused: this id leaves the API's own path.");
|
|
224
|
+
}
|
|
210
225
|
if (params) {
|
|
211
226
|
for (const [key, value] of Object.entries(params)) {
|
|
212
227
|
url.searchParams.set(key, value);
|
|
@@ -216,7 +231,7 @@ var SystemDoxApiClient = class {
|
|
|
216
231
|
}
|
|
217
232
|
async fetchRaw(path, params, signal) {
|
|
218
233
|
const url = this.buildUrl(path, params);
|
|
219
|
-
return
|
|
234
|
+
return this.call(url, {
|
|
220
235
|
headers: {
|
|
221
236
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
222
237
|
"X-API-Key": this.config.apiKey,
|
|
@@ -225,6 +240,16 @@ var SystemDoxApiClient = class {
|
|
|
225
240
|
signal
|
|
226
241
|
});
|
|
227
242
|
}
|
|
243
|
+
/** Every call of this client, so `onAnswer` hears each answer. */
|
|
244
|
+
async call(url, init) {
|
|
245
|
+
const res = await fetch(url, init);
|
|
246
|
+
const { onAnswer } = this.config;
|
|
247
|
+
if (onAnswer) {
|
|
248
|
+
const refused = res.ok ? void 0 : new SystemDoxApiError(res.status, await res.clone().text());
|
|
249
|
+
onAnswer({ status: res.status, code: refused?.code });
|
|
250
|
+
}
|
|
251
|
+
return res;
|
|
252
|
+
}
|
|
228
253
|
};
|
|
229
254
|
|
|
230
255
|
// src/lib/config.ts
|