@systemdox/mcp-server 0.1.42 → 0.1.43
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 +15 -0
- package/dist/cli.js +511 -69
- package/dist/http.js +384 -46
- package/dist/index.js +385 -47
- package/dist/install-skill.d.ts +25 -0
- package/dist/install-skill.js +374 -0
- package/dist/lib/api-client.d.ts +3 -0
- package/dist/lib/api-client.js +10 -0
- package/dist/lib/skills.d.ts +87 -0
- package/dist/lib/skills.js +209 -0
- package/dist/server.d.ts +7 -1
- package/dist/tools/skills.d.ts +5 -0
- package/dist/tools/skills.js +347 -0
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { SystemDoxApiClient } from "./lib/api-client.js";
|
|
3
|
+
export interface ServerOptions {
|
|
4
|
+
/** True only on the user's own computer (stdio): the skill tools may read
|
|
5
|
+
* and write the skill folder there. The hosted server never touches a disk
|
|
6
|
+
* (ADR-216 §2.2). */
|
|
7
|
+
localFiles?: boolean;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* Build a SystemDox MCP server with all tools registered against the given
|
|
5
11
|
* API client. Shared by the stdio entry point (index.ts) and the hosted
|
|
6
12
|
* Streamable HTTP entry point (http.ts).
|
|
7
13
|
*/
|
|
8
|
-
export declare function createServer(apiClient: SystemDoxApiClient): McpServer;
|
|
14
|
+
export declare function createServer(apiClient: SystemDoxApiClient, { localFiles }?: ServerOptions): McpServer;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { SystemDoxApiClient } from "../lib/api-client.js";
|
|
3
|
+
export declare function registerSkillTools(server: McpServer, apiClient: SystemDoxApiClient, { localFiles }: {
|
|
4
|
+
localFiles: boolean;
|
|
5
|
+
}): void;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/tools/skills.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { homedir as homedir2 } from "os";
|
|
6
|
+
import { basename, resolve as resolve2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/lib/config-links.ts
|
|
9
|
+
async function workspaceBase(apiClient) {
|
|
10
|
+
try {
|
|
11
|
+
const access = await apiClient.get(
|
|
12
|
+
"/v1/docs/portal-access"
|
|
13
|
+
);
|
|
14
|
+
const slug = (access.host ?? "").split(".")[0];
|
|
15
|
+
return slug ? `https://${slug}.systemdox.com` : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function configLink(path, base) {
|
|
21
|
+
return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/lib/skills.ts
|
|
25
|
+
import { createHash } from "crypto";
|
|
26
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
27
|
+
import { existsSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
30
|
+
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
31
|
+
var ENTRY_FILE = "SKILL.md";
|
|
32
|
+
var MAX_FILES = 100;
|
|
33
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
34
|
+
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
35
|
+
var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
36
|
+
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
37
|
+
var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
38
|
+
var SkillInstallRefused = class extends Error {
|
|
39
|
+
constructor(dest, edited) {
|
|
40
|
+
super(
|
|
41
|
+
`${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
|
|
42
|
+
);
|
|
43
|
+
this.dest = dest;
|
|
44
|
+
this.edited = edited;
|
|
45
|
+
}
|
|
46
|
+
dest;
|
|
47
|
+
edited;
|
|
48
|
+
};
|
|
49
|
+
function sha256(content) {
|
|
50
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
51
|
+
}
|
|
52
|
+
function isValidName(name) {
|
|
53
|
+
return name.length <= 64 && NAME_RE.test(name);
|
|
54
|
+
}
|
|
55
|
+
function isValidPath(path) {
|
|
56
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
57
|
+
}
|
|
58
|
+
function frontmatterName(skillMd) {
|
|
59
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
60
|
+
if (!block) return void 0;
|
|
61
|
+
const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
|
|
62
|
+
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
63
|
+
}
|
|
64
|
+
function defaultSkillDir(name) {
|
|
65
|
+
return join(homedir(), ".claude", "skills", name);
|
|
66
|
+
}
|
|
67
|
+
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
68
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
69
|
+
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
70
|
+
const full = join(dir, entry.name);
|
|
71
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
72
|
+
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
73
|
+
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
74
|
+
else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
|
|
75
|
+
else if (entry.isFile()) out.files.push(rel);
|
|
76
|
+
}
|
|
77
|
+
out.files.sort();
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
async function readSkillDirectory(directory) {
|
|
81
|
+
const root = resolve(directory);
|
|
82
|
+
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
83
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
84
|
+
}
|
|
85
|
+
const walked = await walk(root, { skipHidden: true });
|
|
86
|
+
if (walked.links.length) {
|
|
87
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
88
|
+
}
|
|
89
|
+
const paths = walked.files;
|
|
90
|
+
if (paths.length > MAX_FILES) {
|
|
91
|
+
throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
|
|
92
|
+
}
|
|
93
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
94
|
+
const files = [];
|
|
95
|
+
let total = 0;
|
|
96
|
+
for (const path of paths) {
|
|
97
|
+
if (!isValidPath(path)) {
|
|
98
|
+
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
99
|
+
}
|
|
100
|
+
const raw = await readFile(join(root, ...path.split("/")));
|
|
101
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
|
|
102
|
+
if (raw.length > MAX_FILE_BYTES) {
|
|
103
|
+
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
104
|
+
}
|
|
105
|
+
total += raw.length;
|
|
106
|
+
if (total > MAX_TOTAL_BYTES) {
|
|
107
|
+
throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
|
|
108
|
+
}
|
|
109
|
+
let content;
|
|
110
|
+
try {
|
|
111
|
+
content = decoder.decode(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`${path} is not UTF-8 text`);
|
|
114
|
+
}
|
|
115
|
+
files.push({ path, content });
|
|
116
|
+
}
|
|
117
|
+
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
118
|
+
const name = frontmatterName(skillMd.content);
|
|
119
|
+
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
120
|
+
return { name, files, skipped: walked.hidden };
|
|
121
|
+
}
|
|
122
|
+
function within(dest, path) {
|
|
123
|
+
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
124
|
+
const full = resolve(dest, ...path.split("/"));
|
|
125
|
+
if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
126
|
+
return full;
|
|
127
|
+
}
|
|
128
|
+
async function readManifest(dest) {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
|
|
131
|
+
} catch {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
136
|
+
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
137
|
+
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
138
|
+
for (const f of version.files) within(dest, f.path);
|
|
139
|
+
if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
140
|
+
throw new Error(`${dest} is a link: install into a real folder`);
|
|
141
|
+
}
|
|
142
|
+
const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
143
|
+
if (walked.links.length) {
|
|
144
|
+
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
145
|
+
}
|
|
146
|
+
const present = walked.files;
|
|
147
|
+
const manifest = await readManifest(dest);
|
|
148
|
+
const known = Object.fromEntries(
|
|
149
|
+
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
150
|
+
);
|
|
151
|
+
const edited = [];
|
|
152
|
+
const untracked = [];
|
|
153
|
+
for (const path of present) {
|
|
154
|
+
const content = await readFile(join(dest, ...path.split("/")), "utf8");
|
|
155
|
+
if (Object.hasOwn(known, path)) {
|
|
156
|
+
if (sha256(content) !== known[path]) edited.push(path);
|
|
157
|
+
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
158
|
+
if (incoming.has(path) || !manifest) edited.push(path);
|
|
159
|
+
else untracked.push(path);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const blocking = edited.filter((p) => incoming.has(p) || !manifest);
|
|
163
|
+
if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
|
|
164
|
+
const written = [];
|
|
165
|
+
for (const f of version.files) {
|
|
166
|
+
const full = within(dest, f.path);
|
|
167
|
+
await mkdir(dirname(full), { recursive: true });
|
|
168
|
+
await writeFile(full, f.content, "utf8");
|
|
169
|
+
written.push(f.path);
|
|
170
|
+
}
|
|
171
|
+
const removed = [];
|
|
172
|
+
const keptEdited = [];
|
|
173
|
+
for (const path of Object.keys(known)) {
|
|
174
|
+
if (incoming.has(path) || !present.includes(path)) continue;
|
|
175
|
+
if (edited.includes(path)) {
|
|
176
|
+
keptEdited.push(path);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
await rm(within(dest, path));
|
|
180
|
+
removed.push(path);
|
|
181
|
+
}
|
|
182
|
+
const next = {
|
|
183
|
+
name: version.name,
|
|
184
|
+
version: version.version,
|
|
185
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
186
|
+
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
187
|
+
};
|
|
188
|
+
await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
189
|
+
return {
|
|
190
|
+
dest,
|
|
191
|
+
version: version.version,
|
|
192
|
+
written,
|
|
193
|
+
removed,
|
|
194
|
+
keptUntracked: untracked.filter((p) => !incoming.has(p)),
|
|
195
|
+
keptEdited
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function fenceFor(text2) {
|
|
199
|
+
const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
|
|
200
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
201
|
+
}
|
|
202
|
+
function describeInstall(name, r) {
|
|
203
|
+
const lines = [
|
|
204
|
+
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
205
|
+
];
|
|
206
|
+
if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
|
|
207
|
+
if (r.keptEdited.length) {
|
|
208
|
+
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
209
|
+
}
|
|
210
|
+
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
211
|
+
lines.push("Start a new Claude Code session to load it.");
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/tools/skills.ts
|
|
216
|
+
var SKILLS = "/v1/standards/skills";
|
|
217
|
+
function expandHome(path) {
|
|
218
|
+
return path.replace(/^~(?=$|[\\/])/, homedir2());
|
|
219
|
+
}
|
|
220
|
+
function text(t) {
|
|
221
|
+
return { content: [{ type: "text", text: t }] };
|
|
222
|
+
}
|
|
223
|
+
function registerSkillTools(server, apiClient, { localFiles }) {
|
|
224
|
+
const fileSchema = z.object({
|
|
225
|
+
path: z.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
226
|
+
content: z.string().describe("The file's full text, exactly as it is")
|
|
227
|
+
});
|
|
228
|
+
server.registerTool(
|
|
229
|
+
"publish_skill",
|
|
230
|
+
{
|
|
231
|
+
title: "Publish Skill",
|
|
232
|
+
description: localFiles ? "Publish a Claude Code skill folder (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass `directory` (e.g. ~/.claude/skills/<name>): the server reads the files itself, byte for byte. Publishing a name again makes a new version. Never retype the files into `files` when the folder is on this computer." : "Publish a Claude Code skill (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass every file of the folder in `files`, exactly as it is. Publishing a name again makes a new version.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
...localFiles ? {
|
|
235
|
+
directory: z.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
236
|
+
} : {},
|
|
237
|
+
files: z.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
238
|
+
name: z.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
239
|
+
spaceId: z.string().optional().describe("Space for a NEW skill (default: your default space)")
|
|
240
|
+
},
|
|
241
|
+
annotations: { readOnlyHint: false }
|
|
242
|
+
},
|
|
243
|
+
async (args) => {
|
|
244
|
+
let files;
|
|
245
|
+
let name;
|
|
246
|
+
let skipped = [];
|
|
247
|
+
if (localFiles && args.directory) {
|
|
248
|
+
({ name, files, skipped } = await readSkillDirectory(expandHome(args.directory)));
|
|
249
|
+
} else if (args.files?.length) {
|
|
250
|
+
files = args.files;
|
|
251
|
+
const entry = files.find((f) => f.path === ENTRY_FILE);
|
|
252
|
+
name = entry ? frontmatterName(entry.content) : void 0;
|
|
253
|
+
} else {
|
|
254
|
+
throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
|
|
255
|
+
}
|
|
256
|
+
name = args.name ?? name;
|
|
257
|
+
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name; pass \`name\`.`);
|
|
258
|
+
const saved = await apiClient.post(
|
|
259
|
+
`${SKILLS}/${encodeURIComponent(name)}/versions`,
|
|
260
|
+
{ files, ...args.spaceId ? { spaceId: args.spaceId } : {} }
|
|
261
|
+
);
|
|
262
|
+
apiClient.invalidateCache(SKILLS);
|
|
263
|
+
return text(
|
|
264
|
+
`Published **${saved.name}** version ${saved.version} (${saved.fileCount} file(s), ${saved.totalBytes} bytes).
|
|
265
|
+
Install it anywhere with: npx -y @systemdox/mcp-server install-skill ${saved.name}` + (skipped.length ? `
|
|
266
|
+
Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
server.registerTool(
|
|
271
|
+
"list_skills",
|
|
272
|
+
{
|
|
273
|
+
title: "List Skills",
|
|
274
|
+
description: "List the Claude Code skills published to SystemDox that you can install: name, description, latest version. Use install_skill to install one.",
|
|
275
|
+
inputSchema: {},
|
|
276
|
+
annotations: { readOnlyHint: true }
|
|
277
|
+
},
|
|
278
|
+
async () => {
|
|
279
|
+
const { skills } = await apiClient.get(SKILLS);
|
|
280
|
+
if (!skills.length) {
|
|
281
|
+
return text(
|
|
282
|
+
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient))}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return text(
|
|
286
|
+
`${skills.length} skill(s):
|
|
287
|
+
|
|
288
|
+
` + skills.map((s) => `- **${s.name}** (version ${s.latestVersion}, updated ${s.updatedAt.slice(0, 10)}): ${s.description}`).join("\n")
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
server.registerTool(
|
|
293
|
+
"install_skill",
|
|
294
|
+
{
|
|
295
|
+
title: "Install Skill",
|
|
296
|
+
description: localFiles ? "Install a skill published to SystemDox onto this computer (default ~/.claude/skills/<name>/). Refuses, naming the files, when a file there was edited since it was installed, unless overwrite is true. Never runs anything." : "Fetch a skill published to SystemDox so you can install it: returns every file and the folder to write them into (default ~/.claude/skills/<name>/). Write each file exactly, then the manifest it shows.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
name: z.string().describe("The skill's name"),
|
|
299
|
+
version: z.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
300
|
+
target: z.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
301
|
+
...localFiles ? { overwrite: z.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
302
|
+
},
|
|
303
|
+
annotations: { readOnlyHint: !localFiles }
|
|
304
|
+
},
|
|
305
|
+
async (args) => {
|
|
306
|
+
const version = await apiClient.getFresh(
|
|
307
|
+
`${SKILLS}/${encodeURIComponent(args.name)}/versions/${args.version ?? "latest"}`
|
|
308
|
+
);
|
|
309
|
+
if (localFiles) {
|
|
310
|
+
const dest2 = args.target ? expandHome(args.target) : defaultSkillDir(version.name);
|
|
311
|
+
if (basename(resolve2(dest2)) !== version.name) {
|
|
312
|
+
throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
|
|
313
|
+
}
|
|
314
|
+
const result = await installSkill(version, dest2, { overwrite: args.overwrite });
|
|
315
|
+
return text(describeInstall(version.name, result));
|
|
316
|
+
}
|
|
317
|
+
const dest = args.target ?? `~/.claude/skills/${version.name}`;
|
|
318
|
+
const manifest = {
|
|
319
|
+
name: version.name,
|
|
320
|
+
version: version.version,
|
|
321
|
+
installedAt: "<now, ISO 8601>",
|
|
322
|
+
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
323
|
+
};
|
|
324
|
+
const body = version.files.map((f) => {
|
|
325
|
+
const fence = fenceFor(f.content);
|
|
326
|
+
return `### ${f.path}
|
|
327
|
+
|
|
328
|
+
${fence}
|
|
329
|
+
${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
330
|
+
}).join("\n\n");
|
|
331
|
+
return text(
|
|
332
|
+
`Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content, and tell the user to start a new Claude Code session.
|
|
333
|
+
|
|
334
|
+
### ${MANIFEST_FILE}
|
|
335
|
+
|
|
336
|
+
\`\`\`json
|
|
337
|
+
${JSON.stringify(manifest, null, 2)}
|
|
338
|
+
\`\`\`
|
|
339
|
+
|
|
340
|
+
${body}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
export {
|
|
346
|
+
registerSkillTools
|
|
347
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemdox/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
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",
|