@kaddo/mcp 3.19.0
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 +90 -0
- package/dist/index.js +606 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @kaddo/mcp
|
|
2
|
+
|
|
3
|
+
A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) server for
|
|
4
|
+
[Kaddo](https://kaddo.trycatch.tv). It exposes your project's curated Kaddo knowledge —
|
|
5
|
+
context pack, explain, understand, knowledge graph, graph hints, Work Items, roadmap, Knowledge
|
|
6
|
+
Capsules and installed agent prompts — to any MCP-compatible client (IDE or agent), so the agent
|
|
7
|
+
gets structured context instead of scanning your whole repository.
|
|
8
|
+
|
|
9
|
+
> Read-only by design. The server never modifies knowledge, edits files, runs git, calls an LLM
|
|
10
|
+
> or scans your source code.
|
|
11
|
+
|
|
12
|
+
## Install & run
|
|
13
|
+
|
|
14
|
+
No install needed — run it with `npx`:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @kaddo/mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The server speaks MCP over **stdio** and operates on the project in its working directory (or the
|
|
21
|
+
directory set via the `KADDO_PROJECT_DIR` environment variable).
|
|
22
|
+
|
|
23
|
+
## Configure an MCP client
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"kaddo": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["@kaddo/mcp"],
|
|
31
|
+
"cwd": "/absolute/path/to/your/project"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`cwd` must point to the project that contains `.kaddo/`, `knowledge/` and (optionally) `external/`.
|
|
38
|
+
|
|
39
|
+
## Resources
|
|
40
|
+
|
|
41
|
+
| URI | Reads | Purpose |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `kaddo://context-pack` | `.kaddo/context-pack.md` | curated LLM context |
|
|
44
|
+
| `kaddo://explain` | `.kaddo/explain.md` | what Kaddo knows |
|
|
45
|
+
| `kaddo://understand` | `.kaddo/understand.md` | current phase + next step |
|
|
46
|
+
| `kaddo://graph` | `.kaddo/graph.json` + `.mmd` | knowledge graph |
|
|
47
|
+
| `kaddo://graph-hints` | `.kaddo/graph-hints.md` + `.json` | weak/missing relationships |
|
|
48
|
+
| `kaddo://work-items` | `knowledge/delivery/work-items/` | summarized Work Items |
|
|
49
|
+
| `kaddo://roadmap` | `knowledge/delivery/roadmap.md` | delivery roadmap |
|
|
50
|
+
| `kaddo://capsules` | `.kaddo/external.yml` + `external/` | external Knowledge Capsules |
|
|
51
|
+
| `kaddo://agents` | `knowledge/agents/` | installed agent prompts |
|
|
52
|
+
| `kaddo://skills` | `knowledge/skills/` | installed skills (empty if none) |
|
|
53
|
+
|
|
54
|
+
## Tools (read-only)
|
|
55
|
+
|
|
56
|
+
- `kaddo_project_status` — compact project status (state, work items, ownership, graph quality, capsules).
|
|
57
|
+
- `kaddo_list_work_items` — filter by `status` / `type` / `knowledge_level`.
|
|
58
|
+
- `kaddo_get_work_item` — a Work Item by `id` (summary + full markdown).
|
|
59
|
+
- `kaddo_list_capsules` / `kaddo_get_capsule` — external Knowledge Capsules.
|
|
60
|
+
- `kaddo_list_agents` / `kaddo_get_agent_prompt` — installed agent prompts.
|
|
61
|
+
- `kaddo_list_graph_hints` — graph hints, filter by `artifact_type` / `severity` / `active_only`.
|
|
62
|
+
|
|
63
|
+
## Prompts
|
|
64
|
+
|
|
65
|
+
Every installed agent prompt (`knowledge/agents/**`) is exposed as an MCP prompt
|
|
66
|
+
(`business-agent`, `work-item-agent`, `implementation-agent`, `graph-agent`, …) with its full
|
|
67
|
+
content and recommended inputs.
|
|
68
|
+
|
|
69
|
+
## What it does NOT do
|
|
70
|
+
|
|
71
|
+
No writes, no Work Item creation, no `kaddo scan`/`context`/`graph export`/`learn`/`owners suggest`,
|
|
72
|
+
no git, no remote sync, no GitHub API, no HTTP server, no auth, no RAG, no vector database. If a
|
|
73
|
+
derived file is missing, the server returns a clear instruction to run the matching CLI command —
|
|
74
|
+
it never generates files.
|
|
75
|
+
|
|
76
|
+
## Security
|
|
77
|
+
|
|
78
|
+
The server only reads `.kaddo/`, `knowledge/` and `external/`. It never reads `src/`, `.git/`,
|
|
79
|
+
`node_modules/`, `dist/`, `build/` or `coverage/`, blocks path traversal, and never exposes
|
|
80
|
+
secrets, tokens, env values, source code or PII.
|
|
81
|
+
|
|
82
|
+
## Troubleshooting
|
|
83
|
+
|
|
84
|
+
- **"Kaddo project not found. Run `kaddo init` first."** — `cwd` is not a Kaddo project. Point it
|
|
85
|
+
at the directory containing `.kaddo/config.yml`.
|
|
86
|
+
- **"… not found. Run `kaddo …` first."** — the derived file hasn't been generated yet. Run the
|
|
87
|
+
named CLI command (e.g. `kaddo context`, `kaddo graph export`).
|
|
88
|
+
- **No prompts listed** — install agents with `kaddo add agents`.
|
|
89
|
+
|
|
90
|
+
Same version as `@kaddo/cli`. See the [MCP Server docs](https://kaddo.trycatch.tv/mcp-server/).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// src/catalog.ts
|
|
11
|
+
import matter from "gray-matter";
|
|
12
|
+
|
|
13
|
+
// src/project.ts
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
var ALLOWED_ROOTS = [".kaddo", "knowledge", "external"];
|
|
18
|
+
var KaddoMcpError = class extends Error {
|
|
19
|
+
};
|
|
20
|
+
function projectRoot() {
|
|
21
|
+
const fromEnv = process.env.KADDO_PROJECT_DIR;
|
|
22
|
+
return path.resolve(fromEnv && fromEnv.trim() ? fromEnv : process.cwd());
|
|
23
|
+
}
|
|
24
|
+
function hasKaddoConfig(root) {
|
|
25
|
+
return fs.existsSync(path.join(root, ".kaddo", "config.yml"));
|
|
26
|
+
}
|
|
27
|
+
function hasKnowledge(root) {
|
|
28
|
+
return fs.existsSync(path.join(root, "knowledge"));
|
|
29
|
+
}
|
|
30
|
+
function assertKaddoProject(root) {
|
|
31
|
+
if (!hasKaddoConfig(root)) {
|
|
32
|
+
throw new KaddoMcpError("Kaddo project not found. Run `kaddo init` first.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function toPosix(p) {
|
|
36
|
+
return p.replace(/\\/g, "/");
|
|
37
|
+
}
|
|
38
|
+
function safeResolve(root, relPath) {
|
|
39
|
+
const rel = toPosix(relPath).replace(/^\/+/, "");
|
|
40
|
+
if (path.isAbsolute(relPath) || rel.split("/").some((seg) => seg === "..")) {
|
|
41
|
+
throw new KaddoMcpError(`Path is not allowed: ${relPath}`);
|
|
42
|
+
}
|
|
43
|
+
const top = rel.split("/")[0];
|
|
44
|
+
if (!ALLOWED_ROOTS.includes(top)) {
|
|
45
|
+
throw new KaddoMcpError(
|
|
46
|
+
`Path is outside the readable Kaddo directories (${ALLOWED_ROOTS.join(", ")}): ${relPath}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const resolved = path.resolve(root, rel);
|
|
50
|
+
const base = toPosix(root).replace(/\/+$/, "");
|
|
51
|
+
if (resolved !== root && !toPosix(resolved).startsWith(base + "/")) {
|
|
52
|
+
throw new KaddoMcpError(`Path traversal blocked: ${relPath}`);
|
|
53
|
+
}
|
|
54
|
+
return resolved;
|
|
55
|
+
}
|
|
56
|
+
function readText(root, relPath) {
|
|
57
|
+
const abs = safeResolve(root, relPath);
|
|
58
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
59
|
+
return fs.readFileSync(abs, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
function readJson(root, relPath) {
|
|
62
|
+
const raw = readText(root, relPath);
|
|
63
|
+
if (raw === null) return null;
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(raw);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function readYaml(root, relPath) {
|
|
71
|
+
const raw = readText(root, relPath);
|
|
72
|
+
if (raw === null) return null;
|
|
73
|
+
try {
|
|
74
|
+
return parseYaml(raw) ?? null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function listFiles(root, relDir, filterExt) {
|
|
80
|
+
let absDir;
|
|
81
|
+
try {
|
|
82
|
+
absDir = safeResolve(root, relDir);
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
const walk = (abs) => {
|
|
89
|
+
for (const entry of fs.readdirSync(abs)) {
|
|
90
|
+
if (entry.startsWith(".")) continue;
|
|
91
|
+
const full = path.join(abs, entry);
|
|
92
|
+
const stat = fs.statSync(full);
|
|
93
|
+
if (stat.isDirectory()) {
|
|
94
|
+
walk(full);
|
|
95
|
+
} else if (stat.isFile() && (!filterExt || entry.endsWith(filterExt))) {
|
|
96
|
+
out.push(toPosix(path.relative(root, full)));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(absDir);
|
|
101
|
+
return out.sort();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/catalog.ts
|
|
105
|
+
function sectionParagraph(md, title) {
|
|
106
|
+
const lines = md.split(/\r?\n/);
|
|
107
|
+
const re = new RegExp(`^##\\s+${title}\\s*$`, "i");
|
|
108
|
+
const start = lines.findIndex((l) => re.test(l));
|
|
109
|
+
if (start < 0) return "";
|
|
110
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
111
|
+
if (/^##\s+/.test(lines[i])) break;
|
|
112
|
+
const t = lines[i].trim();
|
|
113
|
+
if (t && !/^_.*_$/.test(t)) return t;
|
|
114
|
+
}
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
function listCapsules(root) {
|
|
118
|
+
const registry = readYaml(root, ".kaddo/external.yml");
|
|
119
|
+
const entries = Array.isArray(registry?.external) ? registry.external : [];
|
|
120
|
+
return entries.map((e) => {
|
|
121
|
+
const md = e.path ? readText(root, e.path) : null;
|
|
122
|
+
let system;
|
|
123
|
+
let updated_at;
|
|
124
|
+
let purpose;
|
|
125
|
+
if (md) {
|
|
126
|
+
try {
|
|
127
|
+
const { data } = matter(md);
|
|
128
|
+
system = data.system ? String(data.system) : void 0;
|
|
129
|
+
updated_at = data.updated_at ? String(data.updated_at) : void 0;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
purpose = sectionParagraph(md, "Purpose") || void 0;
|
|
133
|
+
}
|
|
134
|
+
return { id: e.id, path: e.path, owner: e.owner, system, updated_at, purpose };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function getCapsule(root, id) {
|
|
138
|
+
const entry = listCapsules(root).find((c) => c.id === id);
|
|
139
|
+
if (!entry) return null;
|
|
140
|
+
const content = readText(root, entry.path);
|
|
141
|
+
if (content === null) return null;
|
|
142
|
+
return { id: entry.id, path: entry.path, content };
|
|
143
|
+
}
|
|
144
|
+
function firstHeadingOrLine(md) {
|
|
145
|
+
for (const line of md.split(/\r?\n/)) {
|
|
146
|
+
const t = line.trim();
|
|
147
|
+
if (!t || t.startsWith("#")) continue;
|
|
148
|
+
return t.replace(/\s+/g, " ").slice(0, 200);
|
|
149
|
+
}
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
function listAgents(root) {
|
|
153
|
+
const files = listFiles(root, "knowledge/agents", "-agent.md");
|
|
154
|
+
return files.map((rel) => {
|
|
155
|
+
const segs = rel.split("/");
|
|
156
|
+
const file = segs[segs.length - 1];
|
|
157
|
+
const group = segs.length >= 3 ? segs[segs.length - 2] : "agents";
|
|
158
|
+
const md = readText(root, rel) ?? "";
|
|
159
|
+
return {
|
|
160
|
+
name: file.replace(/\.md$/, ""),
|
|
161
|
+
path: rel,
|
|
162
|
+
group,
|
|
163
|
+
description: firstHeadingOrLine(md)
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function getAgentPrompt(root, name) {
|
|
168
|
+
const want = name.replace(/\.md$/, "");
|
|
169
|
+
const info = listAgents(root).find((a) => a.name === want);
|
|
170
|
+
if (!info) return null;
|
|
171
|
+
const content = readText(root, info.path);
|
|
172
|
+
if (content === null) return null;
|
|
173
|
+
return { ...info, content };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/workitems.ts
|
|
177
|
+
import matter2 from "gray-matter";
|
|
178
|
+
var ACTIVE_STATES = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
|
|
179
|
+
function strArray(v) {
|
|
180
|
+
return Array.isArray(v) ? v.map((x) => String(x)).filter(Boolean) : [];
|
|
181
|
+
}
|
|
182
|
+
function lifecycleOf(data, relPath) {
|
|
183
|
+
const status = String(data.status ?? "").trim();
|
|
184
|
+
if (status) return status;
|
|
185
|
+
const segs = relPath.split("/");
|
|
186
|
+
const folder = segs[segs.length - 2];
|
|
187
|
+
return ACTIVE_STATES.has(folder) || folder === "completed" || folder === "archived" ? folder : "ready";
|
|
188
|
+
}
|
|
189
|
+
function firstParagraph(body) {
|
|
190
|
+
for (const block of body.split(/\n\s*\n/)) {
|
|
191
|
+
const t = block.trim();
|
|
192
|
+
if (t && !t.startsWith("#") && !t.startsWith(">")) return t.replace(/\s+/g, " ");
|
|
193
|
+
}
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
function listWorkItems(root) {
|
|
197
|
+
const files = listFiles(root, "knowledge/delivery/work-items", ".md");
|
|
198
|
+
const items = [];
|
|
199
|
+
for (const rel of files) {
|
|
200
|
+
const raw = readText(root, rel);
|
|
201
|
+
if (raw === null) continue;
|
|
202
|
+
let parsed;
|
|
203
|
+
try {
|
|
204
|
+
parsed = matter2(raw);
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const data = parsed.data;
|
|
209
|
+
if (!data.type) continue;
|
|
210
|
+
items.push({
|
|
211
|
+
id: String(data.id ?? "") || rel,
|
|
212
|
+
title: String(data.title ?? ""),
|
|
213
|
+
status: lifecycleOf(data, rel),
|
|
214
|
+
type: String(data.type ?? ""),
|
|
215
|
+
knowledge_level: String(data.knowledge_level ?? ""),
|
|
216
|
+
path: rel,
|
|
217
|
+
summary: String(data.summary ?? "") || firstParagraph(parsed.content),
|
|
218
|
+
code: strArray(data.code),
|
|
219
|
+
capabilities: strArray(data.capabilities),
|
|
220
|
+
decisions: strArray(data.decisions),
|
|
221
|
+
capsules: strArray(data.capsules)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return items.sort((a, b) => a.id.localeCompare(b.id));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/resources.ts
|
|
228
|
+
function text(uri, body, mimeType = "text/markdown") {
|
|
229
|
+
return [{ uri, text: body, mimeType }];
|
|
230
|
+
}
|
|
231
|
+
function fileOrHint(root, uri, rel, hint, mimeType = "text/markdown") {
|
|
232
|
+
const body = readText(root, rel);
|
|
233
|
+
return text(uri, body ?? hint, body ? mimeType : "text/plain");
|
|
234
|
+
}
|
|
235
|
+
var RESOURCES = [
|
|
236
|
+
{
|
|
237
|
+
uri: "kaddo://context-pack",
|
|
238
|
+
name: "Kaddo context pack",
|
|
239
|
+
description: "Curated LLM context pack (.kaddo/context-pack.md).",
|
|
240
|
+
mimeType: "text/markdown",
|
|
241
|
+
read: (root) => fileOrHint(
|
|
242
|
+
root,
|
|
243
|
+
"kaddo://context-pack",
|
|
244
|
+
".kaddo/context-pack.md",
|
|
245
|
+
"Context pack not found. Run `kaddo context` in the project first."
|
|
246
|
+
)
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
uri: "kaddo://explain",
|
|
250
|
+
name: "Kaddo explain",
|
|
251
|
+
description: "What Kaddo knows about the project (.kaddo/explain.md).",
|
|
252
|
+
mimeType: "text/markdown",
|
|
253
|
+
read: (root) => fileOrHint(
|
|
254
|
+
root,
|
|
255
|
+
"kaddo://explain",
|
|
256
|
+
".kaddo/explain.md",
|
|
257
|
+
"Explain output not found. Run `kaddo explain` first."
|
|
258
|
+
)
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
uri: "kaddo://understand",
|
|
262
|
+
name: "Kaddo understand",
|
|
263
|
+
description: "Current phase and recommended next step (.kaddo/understand.md).",
|
|
264
|
+
mimeType: "text/markdown",
|
|
265
|
+
read: (root) => fileOrHint(
|
|
266
|
+
root,
|
|
267
|
+
"kaddo://understand",
|
|
268
|
+
".kaddo/understand.md",
|
|
269
|
+
"Understand output not found. Run `kaddo understand` first."
|
|
270
|
+
)
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
uri: "kaddo://graph",
|
|
274
|
+
name: "Kaddo knowledge graph",
|
|
275
|
+
description: "Knowledge graph (.kaddo/graph.json + .kaddo/graph.mmd).",
|
|
276
|
+
mimeType: "application/json",
|
|
277
|
+
read: (root) => {
|
|
278
|
+
const json = readText(root, ".kaddo/graph.json");
|
|
279
|
+
const mmd = readText(root, ".kaddo/graph.mmd");
|
|
280
|
+
if (!json && !mmd) {
|
|
281
|
+
return text("kaddo://graph", "Knowledge graph not found. Run `kaddo graph export` first.", "text/plain");
|
|
282
|
+
}
|
|
283
|
+
const parts = [];
|
|
284
|
+
if (json) parts.push({ uri: "kaddo://graph", text: json, mimeType: "application/json" });
|
|
285
|
+
if (mmd) parts.push({ uri: "kaddo://graph.mmd", text: mmd, mimeType: "text/vnd.mermaid" });
|
|
286
|
+
return parts;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
uri: "kaddo://graph-hints",
|
|
291
|
+
name: "Kaddo graph hints",
|
|
292
|
+
description: "Graph relationship-quality hints (.kaddo/graph-hints.md + .json).",
|
|
293
|
+
mimeType: "text/markdown",
|
|
294
|
+
read: (root) => {
|
|
295
|
+
const md = readText(root, ".kaddo/graph-hints.md");
|
|
296
|
+
const json = readText(root, ".kaddo/graph-hints.json");
|
|
297
|
+
if (!md && !json) {
|
|
298
|
+
return text("kaddo://graph-hints", "Graph hints not found. Run `kaddo graph export` first.", "text/plain");
|
|
299
|
+
}
|
|
300
|
+
const parts = [];
|
|
301
|
+
if (md) parts.push({ uri: "kaddo://graph-hints", text: md, mimeType: "text/markdown" });
|
|
302
|
+
if (json) parts.push({ uri: "kaddo://graph-hints.json", text: json, mimeType: "application/json" });
|
|
303
|
+
return parts;
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
uri: "kaddo://work-items",
|
|
308
|
+
name: "Kaddo work items",
|
|
309
|
+
description: "Summarized Work Items from knowledge/delivery/work-items/.",
|
|
310
|
+
mimeType: "application/json",
|
|
311
|
+
read: (root) => {
|
|
312
|
+
if (!hasKnowledge(root)) {
|
|
313
|
+
return text("kaddo://work-items", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
|
|
314
|
+
}
|
|
315
|
+
return [
|
|
316
|
+
{
|
|
317
|
+
uri: "kaddo://work-items",
|
|
318
|
+
text: JSON.stringify(listWorkItems(root), null, 2),
|
|
319
|
+
mimeType: "application/json"
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
uri: "kaddo://roadmap",
|
|
326
|
+
name: "Kaddo roadmap",
|
|
327
|
+
description: "Delivery roadmap (knowledge/delivery/roadmap.md).",
|
|
328
|
+
mimeType: "text/markdown",
|
|
329
|
+
read: (root) => fileOrHint(
|
|
330
|
+
root,
|
|
331
|
+
"kaddo://roadmap",
|
|
332
|
+
"knowledge/delivery/roadmap.md",
|
|
333
|
+
"Roadmap not found. Create knowledge/delivery/roadmap.md (e.g. via the roadmap-agent) first."
|
|
334
|
+
)
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
uri: "kaddo://capsules",
|
|
338
|
+
name: "Kaddo knowledge capsules",
|
|
339
|
+
description: "External Knowledge Capsules from .kaddo/external.yml and external/.",
|
|
340
|
+
mimeType: "application/json",
|
|
341
|
+
read: (root) => [
|
|
342
|
+
{
|
|
343
|
+
uri: "kaddo://capsules",
|
|
344
|
+
text: JSON.stringify(listCapsules(root), null, 2),
|
|
345
|
+
mimeType: "application/json"
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
uri: "kaddo://agents",
|
|
351
|
+
name: "Kaddo agents",
|
|
352
|
+
description: "Installed agent prompts from knowledge/agents/.",
|
|
353
|
+
mimeType: "application/json",
|
|
354
|
+
read: (root) => [
|
|
355
|
+
{
|
|
356
|
+
uri: "kaddo://agents",
|
|
357
|
+
text: JSON.stringify(listAgents(root), null, 2),
|
|
358
|
+
mimeType: "application/json"
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
uri: "kaddo://skills",
|
|
364
|
+
name: "Kaddo skills",
|
|
365
|
+
description: "Installed skills from knowledge/skills/ (empty if none).",
|
|
366
|
+
mimeType: "application/json",
|
|
367
|
+
read: (root) => {
|
|
368
|
+
const files = listFiles(root, "knowledge/skills", ".md");
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
uri: "kaddo://skills",
|
|
372
|
+
text: JSON.stringify({ skills: files }, null, 2),
|
|
373
|
+
mimeType: "application/json"
|
|
374
|
+
}
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
// src/prompts.ts
|
|
381
|
+
var EXTRA_INPUTS = {
|
|
382
|
+
"graph-agent": [".kaddo/graph.json", ".kaddo/graph-hints.md"],
|
|
383
|
+
"ownership-agent": ["knowledge/delivery/work-items/", "knowledge/inventory.md"],
|
|
384
|
+
"capsule-agent": ["knowledge/product/capabilities.md", "knowledge/tech/decisions/"],
|
|
385
|
+
"work-item-agent": ["knowledge/delivery/roadmap.md"],
|
|
386
|
+
"implementation-agent": ["knowledge/tech/git-strategy.md"]
|
|
387
|
+
};
|
|
388
|
+
function recommendedInputs(name) {
|
|
389
|
+
return [".kaddo/context-pack.md", ...EXTRA_INPUTS[name] ?? []];
|
|
390
|
+
}
|
|
391
|
+
function listPrompts(root) {
|
|
392
|
+
return listAgents(root).map((a) => ({
|
|
393
|
+
name: a.name,
|
|
394
|
+
description: a.description || `Kaddo ${a.name}`,
|
|
395
|
+
content: "",
|
|
396
|
+
recommended_inputs: recommendedInputs(a.name)
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
function getPrompt(root, name) {
|
|
400
|
+
const agent = getAgentPrompt(root, name);
|
|
401
|
+
if (!agent) return null;
|
|
402
|
+
return {
|
|
403
|
+
name: agent.name,
|
|
404
|
+
description: agent.description || `Kaddo ${agent.name}`,
|
|
405
|
+
content: agent.content,
|
|
406
|
+
recommended_inputs: recommendedInputs(agent.name)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/tools.ts
|
|
411
|
+
var ok = (data) => ({ ok: true, data });
|
|
412
|
+
var fail = (message) => ({ ok: false, message });
|
|
413
|
+
function projectStatus(root) {
|
|
414
|
+
const explain = readJson(root, ".kaddo/explain.json");
|
|
415
|
+
const hints = readJson(root, ".kaddo/graph-hints.json");
|
|
416
|
+
const capsules = listCapsules(root);
|
|
417
|
+
if (!explain) {
|
|
418
|
+
return fail("Project status needs `.kaddo/explain.json`. Run `kaddo explain` first.");
|
|
419
|
+
}
|
|
420
|
+
const maturity = (explain.layers ?? []).map((l) => `${l.layer}: ${l.status}`);
|
|
421
|
+
return ok({
|
|
422
|
+
project: explain.project?.name ?? "unknown",
|
|
423
|
+
state: explain.project?.state ?? "unknown",
|
|
424
|
+
knowledgeMaturity: maturity,
|
|
425
|
+
workItems: {
|
|
426
|
+
total: explain.workItems?.total ?? 0,
|
|
427
|
+
byState: explain.workItems?.byState ?? {},
|
|
428
|
+
byType: explain.workItems?.byType ?? {}
|
|
429
|
+
},
|
|
430
|
+
ownership: {
|
|
431
|
+
withOwnership: explain.ownership?.workItemsWithOwnership ?? 0,
|
|
432
|
+
total: explain.ownership?.workItemsTotal ?? 0
|
|
433
|
+
},
|
|
434
|
+
graphQuality: hints?.quality ?? "unknown (run `kaddo graph export`)",
|
|
435
|
+
graphHints: hints?.summary?.hints ?? 0,
|
|
436
|
+
capsules: capsules.length
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function listWorkItemsTool(root, filter = {}) {
|
|
440
|
+
let items = listWorkItems(root);
|
|
441
|
+
if (filter.status) items = items.filter((w) => w.status === filter.status);
|
|
442
|
+
if (filter.type) items = items.filter((w) => w.type === filter.type);
|
|
443
|
+
if (filter.knowledge_level) items = items.filter((w) => w.knowledge_level === filter.knowledge_level);
|
|
444
|
+
return ok(items);
|
|
445
|
+
}
|
|
446
|
+
function getWorkItem(root, id) {
|
|
447
|
+
const item = listWorkItems(root).find((w) => w.id === id);
|
|
448
|
+
if (!item) return fail(`Work Item "${id}" not found.`);
|
|
449
|
+
const content = readText(root, item.path);
|
|
450
|
+
return ok({ ...item, content: content ?? "" });
|
|
451
|
+
}
|
|
452
|
+
function listCapsulesTool(root) {
|
|
453
|
+
return ok(listCapsules(root));
|
|
454
|
+
}
|
|
455
|
+
function getCapsuleTool(root, id) {
|
|
456
|
+
const cap = getCapsule(root, id);
|
|
457
|
+
if (!cap) return fail(`Knowledge Capsule "${id}" not found.`);
|
|
458
|
+
return ok(cap);
|
|
459
|
+
}
|
|
460
|
+
function listAgentsTool(root) {
|
|
461
|
+
return ok(listAgents(root));
|
|
462
|
+
}
|
|
463
|
+
function getAgentPromptTool(root, name) {
|
|
464
|
+
const agent = getAgentPrompt(root, name);
|
|
465
|
+
if (!agent) return fail(`Agent "${name}" is not installed. Run \`kaddo add agents\` first.`);
|
|
466
|
+
return ok(agent);
|
|
467
|
+
}
|
|
468
|
+
function listGraphHints(root, filter = {}) {
|
|
469
|
+
const report = readJson(root, ".kaddo/graph-hints.json");
|
|
470
|
+
if (!report) return fail("Graph hints not found. Run `kaddo graph export` first.");
|
|
471
|
+
let hints = Array.isArray(report.hints) ? report.hints : [];
|
|
472
|
+
if (filter.artifact_type) hints = hints.filter((h) => h.artifact_type === filter.artifact_type);
|
|
473
|
+
if (filter.severity) hints = hints.filter((h) => h.severity === filter.severity);
|
|
474
|
+
if (filter.active_only) hints = hints.filter((h) => h.artifact_type === "work-item");
|
|
475
|
+
return ok({ quality: report.quality ?? "unknown", count: hints.length, hints });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/server.ts
|
|
479
|
+
var SERVER_NAME = "kaddo";
|
|
480
|
+
var SERVER_VERSION = "3.19.0";
|
|
481
|
+
function toolText(result) {
|
|
482
|
+
if (!result.ok) {
|
|
483
|
+
return { content: [{ type: "text", text: result.message }], isError: true };
|
|
484
|
+
}
|
|
485
|
+
const text2 = typeof result.data === "string" ? result.data : JSON.stringify(result.data, null, 2);
|
|
486
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
487
|
+
}
|
|
488
|
+
function guarded(root, fn) {
|
|
489
|
+
try {
|
|
490
|
+
assertKaddoProject(root);
|
|
491
|
+
return fn();
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (err instanceof KaddoMcpError) return { ok: false, message: err.message };
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function createServer(root) {
|
|
498
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
499
|
+
for (const r of RESOURCES) {
|
|
500
|
+
server.registerResource(
|
|
501
|
+
r.name,
|
|
502
|
+
r.uri,
|
|
503
|
+
{ title: r.name, description: r.description, mimeType: r.mimeType },
|
|
504
|
+
async (uri) => {
|
|
505
|
+
try {
|
|
506
|
+
assertKaddoProject(root);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
const message = err instanceof KaddoMcpError ? err.message : String(err);
|
|
509
|
+
return { contents: [{ uri: uri.href, text: message, mimeType: "text/plain" }] };
|
|
510
|
+
}
|
|
511
|
+
const parts = r.read(root);
|
|
512
|
+
return { contents: parts.map((p) => ({ uri: p.uri, text: p.text, mimeType: p.mimeType })) };
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
server.registerTool(
|
|
517
|
+
"kaddo_project_status",
|
|
518
|
+
{ title: "Kaddo project status", description: "Compact project status (state, phase, work items, ownership, graph quality, capsules).", inputSchema: {} },
|
|
519
|
+
async () => toolText(guarded(root, () => projectStatus(root)))
|
|
520
|
+
);
|
|
521
|
+
server.registerTool(
|
|
522
|
+
"kaddo_list_work_items",
|
|
523
|
+
{
|
|
524
|
+
title: "List Work Items",
|
|
525
|
+
description: "List Work Items with optional status/type/knowledge_level filters.",
|
|
526
|
+
inputSchema: {
|
|
527
|
+
status: z.string().optional(),
|
|
528
|
+
type: z.string().optional(),
|
|
529
|
+
knowledge_level: z.string().optional()
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
async (args) => toolText(guarded(root, () => listWorkItemsTool(root, args)))
|
|
533
|
+
);
|
|
534
|
+
server.registerTool(
|
|
535
|
+
"kaddo_get_work_item",
|
|
536
|
+
{ title: "Get Work Item", description: "Get a Work Item by ID (summary + full markdown).", inputSchema: { id: z.string() } },
|
|
537
|
+
async (args) => toolText(guarded(root, () => getWorkItem(root, args.id)))
|
|
538
|
+
);
|
|
539
|
+
server.registerTool(
|
|
540
|
+
"kaddo_list_capsules",
|
|
541
|
+
{ title: "List Knowledge Capsules", description: "List registered external Knowledge Capsules.", inputSchema: {} },
|
|
542
|
+
async () => toolText(guarded(root, () => listCapsulesTool(root)))
|
|
543
|
+
);
|
|
544
|
+
server.registerTool(
|
|
545
|
+
"kaddo_get_capsule",
|
|
546
|
+
{ title: "Get Knowledge Capsule", description: "Get an external Knowledge Capsule by ID.", inputSchema: { id: z.string() } },
|
|
547
|
+
async (args) => toolText(guarded(root, () => getCapsuleTool(root, args.id)))
|
|
548
|
+
);
|
|
549
|
+
server.registerTool(
|
|
550
|
+
"kaddo_list_agents",
|
|
551
|
+
{ title: "List agents", description: "List installed agent prompts.", inputSchema: {} },
|
|
552
|
+
async () => toolText(guarded(root, () => listAgentsTool(root)))
|
|
553
|
+
);
|
|
554
|
+
server.registerTool(
|
|
555
|
+
"kaddo_get_agent_prompt",
|
|
556
|
+
{ title: "Get agent prompt", description: "Get an installed agent prompt by name.", inputSchema: { name: z.string() } },
|
|
557
|
+
async (args) => toolText(guarded(root, () => getAgentPromptTool(root, args.name)))
|
|
558
|
+
);
|
|
559
|
+
server.registerTool(
|
|
560
|
+
"kaddo_list_graph_hints",
|
|
561
|
+
{
|
|
562
|
+
title: "List graph hints",
|
|
563
|
+
description: "List knowledge-graph relationship hints with optional filters.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
artifact_type: z.string().optional(),
|
|
566
|
+
severity: z.string().optional(),
|
|
567
|
+
active_only: z.boolean().optional()
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
async (args) => toolText(guarded(root, () => listGraphHints(root, args)))
|
|
571
|
+
);
|
|
572
|
+
let prompts = [];
|
|
573
|
+
try {
|
|
574
|
+
assertKaddoProject(root);
|
|
575
|
+
prompts = listPrompts(root);
|
|
576
|
+
} catch {
|
|
577
|
+
prompts = [];
|
|
578
|
+
}
|
|
579
|
+
for (const p of prompts) {
|
|
580
|
+
server.registerPrompt(
|
|
581
|
+
p.name,
|
|
582
|
+
{ title: p.name, description: p.description },
|
|
583
|
+
async () => {
|
|
584
|
+
const full = getPrompt(root, p.name);
|
|
585
|
+
const content = full?.content ?? `Agent "${p.name}" is not installed.`;
|
|
586
|
+
return { messages: [{ role: "user", content: { type: "text", text: content } }] };
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return server;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/index.ts
|
|
594
|
+
async function main() {
|
|
595
|
+
const root = projectRoot();
|
|
596
|
+
const server = createServer(root);
|
|
597
|
+
const transport = new StdioServerTransport();
|
|
598
|
+
await server.connect(transport);
|
|
599
|
+
process.stderr.write(`[kaddo-mcp] read-only server ready (project: ${root})
|
|
600
|
+
`);
|
|
601
|
+
}
|
|
602
|
+
main().catch((err) => {
|
|
603
|
+
process.stderr.write(`[kaddo-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
604
|
+
`);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaddo/mcp",
|
|
3
|
+
"version": "3.19.0",
|
|
4
|
+
"description": "Read-only MCP server for Kaddo project knowledge.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Kaddo-kdd/kaddo.git",
|
|
9
|
+
"directory": "packages/mcp"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://kaddo.trycatch.tv",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Kaddo-kdd/kaddo/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"kdd",
|
|
17
|
+
"knowledge",
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"kaddo"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"bin": {
|
|
25
|
+
"kaddo-mcp": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"gray-matter": "^4.0.3",
|
|
43
|
+
"yaml": "^2.4.2",
|
|
44
|
+
"zod": "^3.23.8"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.14.0",
|
|
48
|
+
"tsup": "^8.1.0",
|
|
49
|
+
"typescript": "^5.4.5"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
}
|
|
54
|
+
}
|