@shaferllc/keel 0.66.0 → 0.68.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/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +2 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +26 -7
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Keel MCP server. Exposes Keel's documentation, public API surface, and
|
|
3
|
+
* code generators to AI agents (Claude Code, Cursor, …) over stdio, so an agent
|
|
4
|
+
* editing a Keel app can look things up and scaffold code without guessing.
|
|
5
|
+
*
|
|
6
|
+
* Run it with `keel mcp` (dev) or the shipped `keel-mcp` bin (consumers).
|
|
7
|
+
* Tools:
|
|
8
|
+
* keel_overview framework facts, conventions, folder layout
|
|
9
|
+
* keel_search_docs full-text search across the guides
|
|
10
|
+
* keel_read_doc a full guide (optionally with its runnable example)
|
|
11
|
+
* keel_search_api search the public export surface
|
|
12
|
+
* keel_list_generators the `keel make:*` generators
|
|
13
|
+
* keel_scaffold generate a controller/provider/job/… stub (no write)
|
|
14
|
+
* Resources: keel://overview, keel://llms-full, and keel://docs/<slug> per guide.
|
|
15
|
+
*/
|
|
16
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { controllerStub, resourceControllerStub, providerStub, middlewareStub, factoryStub, seederStub, jobStub, notificationStub, transformerStub, } from "../core/cli/stubs.js";
|
|
23
|
+
async function exists(p) {
|
|
24
|
+
try {
|
|
25
|
+
await stat(p);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Walk up from this module until we find the package root (the dir holding docs/). */
|
|
33
|
+
async function findRoot() {
|
|
34
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
for (let i = 0; i < 6; i++) {
|
|
36
|
+
if (await exists(join(dir, "docs")))
|
|
37
|
+
return dir;
|
|
38
|
+
const parent = dirname(dir);
|
|
39
|
+
if (parent === dir)
|
|
40
|
+
break;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
// Fall back to two levels up (…/mcp/server.js -> package root).
|
|
44
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
45
|
+
}
|
|
46
|
+
/** Load the generated manifest, or reconstruct a minimal one by scanning docs/. */
|
|
47
|
+
async function loadManifest(root) {
|
|
48
|
+
const manifestPath = join(root, "docs", "ai-manifest.json");
|
|
49
|
+
if (await exists(manifestPath)) {
|
|
50
|
+
return JSON.parse(await readFile(manifestPath, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
// Fallback: scan docs so the server still works before `npm run build:ai`.
|
|
53
|
+
const docsDir = join(root, "docs");
|
|
54
|
+
const files = (await readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
|
|
55
|
+
const docs = [];
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const md = await readFile(join(docsDir, file), "utf8");
|
|
58
|
+
const title = md.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? file;
|
|
59
|
+
docs.push({ slug: file.replace(/\.md$/, ""), title, summary: "", path: `docs/${file}`, example: null });
|
|
60
|
+
}
|
|
61
|
+
let pkg = { name: "@shaferllc/keel", version: "0.0.0", description: "The house framework for Node.js." };
|
|
62
|
+
try {
|
|
63
|
+
pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* use defaults */
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
name: pkg.name,
|
|
70
|
+
version: pkg.version,
|
|
71
|
+
description: pkg.description,
|
|
72
|
+
repo: "https://github.com/shaferllc/keel",
|
|
73
|
+
docs,
|
|
74
|
+
api: [],
|
|
75
|
+
generators: [],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// ---- generators ------------------------------------------------------------
|
|
79
|
+
/** Normalize "foo" / "FooController" into a canonical suffixed class name. */
|
|
80
|
+
function className(name, suffix) {
|
|
81
|
+
const base = name.replace(new RegExp(`${suffix}$`, "i"), "");
|
|
82
|
+
const pascal = base.charAt(0).toUpperCase() + base.slice(1);
|
|
83
|
+
return `${pascal}${suffix}`;
|
|
84
|
+
}
|
|
85
|
+
const GENERATOR_KINDS = [
|
|
86
|
+
"controller",
|
|
87
|
+
"provider",
|
|
88
|
+
"middleware",
|
|
89
|
+
"factory",
|
|
90
|
+
"seeder",
|
|
91
|
+
"job",
|
|
92
|
+
"notification",
|
|
93
|
+
"transformer",
|
|
94
|
+
];
|
|
95
|
+
/** Build a stub for a `make:*` kind. Pure — returns the code and target path, never writes. */
|
|
96
|
+
function scaffold(kind, name, opts) {
|
|
97
|
+
switch (kind) {
|
|
98
|
+
case "controller": {
|
|
99
|
+
const cls = className(name, "Controller");
|
|
100
|
+
return {
|
|
101
|
+
path: `app/Controllers/${cls}.ts`,
|
|
102
|
+
code: opts.resource ? resourceControllerStub(cls) : controllerStub(cls),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "provider": {
|
|
106
|
+
const cls = className(name, "ServiceProvider");
|
|
107
|
+
return { path: `app/Providers/${cls}.ts`, code: providerStub(cls) };
|
|
108
|
+
}
|
|
109
|
+
case "middleware": {
|
|
110
|
+
const cls = className(name, "Middleware");
|
|
111
|
+
const file = cls.charAt(0).toLowerCase() + cls.slice(1);
|
|
112
|
+
return { path: `app/Http/Middleware/${file}.ts`, code: middlewareStub(cls) };
|
|
113
|
+
}
|
|
114
|
+
case "factory": {
|
|
115
|
+
const cls = className(name, "");
|
|
116
|
+
return { path: `database/factories/${cls}Factory.ts`, code: factoryStub(cls) };
|
|
117
|
+
}
|
|
118
|
+
case "seeder": {
|
|
119
|
+
const cls = className(name, "Seeder");
|
|
120
|
+
return { path: `database/seeders/${cls}.ts`, code: seederStub(cls) };
|
|
121
|
+
}
|
|
122
|
+
case "job": {
|
|
123
|
+
const cls = className(name, "Job");
|
|
124
|
+
return { path: `app/Jobs/${cls}.ts`, code: jobStub(cls) };
|
|
125
|
+
}
|
|
126
|
+
case "notification": {
|
|
127
|
+
const cls = className(name, "Notification");
|
|
128
|
+
return { path: `app/Notifications/${cls}.ts`, code: notificationStub(cls) };
|
|
129
|
+
}
|
|
130
|
+
case "transformer": {
|
|
131
|
+
const cls = className(name, "Transformer");
|
|
132
|
+
const model = opts.model ? className(opts.model, "") : cls.replace(/Transformer$/, "");
|
|
133
|
+
return { path: `app/Transformers/${cls}.ts`, code: transformerStub(cls, model) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---- search ----------------------------------------------------------------
|
|
138
|
+
function snippet(body, query) {
|
|
139
|
+
const idx = body.toLowerCase().indexOf(query.toLowerCase());
|
|
140
|
+
if (idx === -1)
|
|
141
|
+
return body.slice(0, 200).replace(/\s+/g, " ").trim();
|
|
142
|
+
const start = Math.max(0, idx - 80);
|
|
143
|
+
const end = Math.min(body.length, idx + query.length + 120);
|
|
144
|
+
return (start > 0 ? "…" : "") + body.slice(start, end).replace(/\s+/g, " ").trim() + (end < body.length ? "…" : "");
|
|
145
|
+
}
|
|
146
|
+
// ---- server ----------------------------------------------------------------
|
|
147
|
+
export async function createServer() {
|
|
148
|
+
const root = await findRoot();
|
|
149
|
+
const manifest = await loadManifest(root);
|
|
150
|
+
const docBySlug = new Map(manifest.docs.map((d) => [d.slug, d]));
|
|
151
|
+
// Which doc guide, if any, documents a given source module (e.g. "queue" -> "queues").
|
|
152
|
+
const moduleToDoc = (module) => {
|
|
153
|
+
for (const cand of [module, `${module}s`, module.replace(/s$/, "")]) {
|
|
154
|
+
if (docBySlug.has(cand))
|
|
155
|
+
return cand;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
};
|
|
159
|
+
const server = new McpServer({ name: "keel", version: manifest.version });
|
|
160
|
+
const overviewText = () => [
|
|
161
|
+
`# ${manifest.name} v${manifest.version}`,
|
|
162
|
+
"",
|
|
163
|
+
manifest.description,
|
|
164
|
+
"",
|
|
165
|
+
"Keel is a small, legible MVC framework for Node.js. Hono powers HTTP; everything",
|
|
166
|
+
"above it is Keel's. Userland imports everything from `@shaferllc/keel/core`.",
|
|
167
|
+
"",
|
|
168
|
+
"## Conventions",
|
|
169
|
+
"- Import surface: `import { Router, Model, config } from \"@shaferllc/keel/core\";`",
|
|
170
|
+
"- Everything resolves through the service container (`bind`/`singleton`/`make`).",
|
|
171
|
+
"- Service providers (`register()` then `boot()`) wire the app together.",
|
|
172
|
+
"- Config is dot-notation: `config(\"app.name\")`, sourced from `config/*.ts` + `.env`.",
|
|
173
|
+
"",
|
|
174
|
+
"## Folder layout (a Keel app)",
|
|
175
|
+
"- `app/Controllers`, `app/Providers`, `app/Http/Middleware`, `app/Models`, `app/Jobs`, …",
|
|
176
|
+
"- `config/*.ts` — configuration files",
|
|
177
|
+
"- `routes/web.ts` — route definitions (default export receives the Router)",
|
|
178
|
+
"- `database/factories`, `database/seeders`, `database/migrations`",
|
|
179
|
+
"- `bootstrap/app.ts` — assembles the Application and its providers",
|
|
180
|
+
"",
|
|
181
|
+
`## Guides (${manifest.docs.length}) — read with keel_read_doc`,
|
|
182
|
+
...manifest.docs.map((d) => `- ${d.slug}: ${d.summary || d.title}`),
|
|
183
|
+
"",
|
|
184
|
+
"## Generators — use keel_scaffold or `keel make:*`",
|
|
185
|
+
...manifest.generators.map((g) => `- ${g.command} → ${g.produces}`),
|
|
186
|
+
].join("\n");
|
|
187
|
+
// ---- tools ----
|
|
188
|
+
server.registerTool("keel_overview", {
|
|
189
|
+
title: "Keel overview",
|
|
190
|
+
description: "Start here. Returns Keel's version, conventions, folder layout, the full list of doc topics, and the available code generators. Call this first when working in a Keel app.",
|
|
191
|
+
inputSchema: {},
|
|
192
|
+
}, async () => ({ content: [{ type: "text", text: overviewText() }] }));
|
|
193
|
+
server.registerTool("keel_search_docs", {
|
|
194
|
+
title: "Search Keel docs",
|
|
195
|
+
description: "Full-text search across all Keel guides. Returns the best-matching guides with a snippet and slug; follow up with keel_read_doc to read one in full.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
query: z.string().describe("What to look for, e.g. 'rate limit middleware' or 'belongsToMany'"),
|
|
198
|
+
limit: z.number().int().min(1).max(20).optional().describe("Max results (default 6)"),
|
|
199
|
+
},
|
|
200
|
+
}, async ({ query, limit }) => {
|
|
201
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
202
|
+
const first = terms[0] ?? query.toLowerCase();
|
|
203
|
+
const scored = [];
|
|
204
|
+
for (const doc of manifest.docs) {
|
|
205
|
+
const body = await readFile(join(root, doc.path), "utf8");
|
|
206
|
+
const hay = body.toLowerCase();
|
|
207
|
+
const title = doc.title.toLowerCase();
|
|
208
|
+
let score = 0;
|
|
209
|
+
for (const t of terms) {
|
|
210
|
+
if (title.includes(t))
|
|
211
|
+
score += 10;
|
|
212
|
+
if (doc.slug.includes(t))
|
|
213
|
+
score += 8;
|
|
214
|
+
const matches = hay.split(t).length - 1;
|
|
215
|
+
score += Math.min(matches, 8);
|
|
216
|
+
}
|
|
217
|
+
if (score > 0)
|
|
218
|
+
scored.push({ doc, score, body });
|
|
219
|
+
}
|
|
220
|
+
scored.sort((a, b) => b.score - a.score);
|
|
221
|
+
const top = scored.slice(0, limit ?? 6);
|
|
222
|
+
if (top.length === 0) {
|
|
223
|
+
return { content: [{ type: "text", text: `No Keel docs matched "${query}".` }] };
|
|
224
|
+
}
|
|
225
|
+
const text = top
|
|
226
|
+
.map(({ doc, body }) => `## ${doc.title} (slug: ${doc.slug})\n${snippet(body, first)}\n→ keel_read_doc { slug: "${doc.slug}" }`)
|
|
227
|
+
.join("\n\n");
|
|
228
|
+
return { content: [{ type: "text", text }] };
|
|
229
|
+
});
|
|
230
|
+
server.registerTool("keel_read_doc", {
|
|
231
|
+
title: "Read a Keel doc",
|
|
232
|
+
description: "Return a full Keel guide by slug (e.g. 'routing', 'models', 'queues'). Optionally append its runnable, type-checked example. Use keel_search_docs or keel_overview to find slugs.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
slug: z.string().describe("Doc slug, e.g. 'routing' or 'getting-started'"),
|
|
235
|
+
include_example: z.boolean().optional().describe("Append the guide's example source if it has one (default false)"),
|
|
236
|
+
},
|
|
237
|
+
}, async ({ slug, include_example }) => {
|
|
238
|
+
const doc = docBySlug.get(slug);
|
|
239
|
+
if (!doc) {
|
|
240
|
+
const near = manifest.docs
|
|
241
|
+
.filter((d) => d.slug.includes(slug) || slug.includes(d.slug))
|
|
242
|
+
.map((d) => d.slug);
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: `No doc "${slug}".${near.length ? ` Did you mean: ${near.join(", ")}?` : ""} Use keel_overview to list all slugs.`,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
isError: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
let text = await readFile(join(root, doc.path), "utf8");
|
|
254
|
+
if (include_example && doc.example && (await exists(join(root, doc.example)))) {
|
|
255
|
+
const ex = await readFile(join(root, doc.example), "utf8");
|
|
256
|
+
text += `\n\n---\n\n## Example — ${doc.example}\n\n\`\`\`ts\n${ex}\n\`\`\`\n`;
|
|
257
|
+
}
|
|
258
|
+
return { content: [{ type: "text", text }] };
|
|
259
|
+
});
|
|
260
|
+
server.registerTool("keel_search_api", {
|
|
261
|
+
title: "Search Keel's public API",
|
|
262
|
+
description: "Search the public export surface of `@shaferllc/keel/core` (382+ symbols). Returns matching value/type exports, the source module each lives in, and the guide that documents it.",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
query: z.string().describe("A symbol or fragment, e.g. 'Router', 'cache', 'hasMany', 'Exception'"),
|
|
265
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results (default 20)"),
|
|
266
|
+
},
|
|
267
|
+
}, async ({ query, limit }) => {
|
|
268
|
+
if (manifest.api.length === 0) {
|
|
269
|
+
return { content: [{ type: "text", text: "API index unavailable — run `npm run build:ai`." }] };
|
|
270
|
+
}
|
|
271
|
+
const q = query.toLowerCase();
|
|
272
|
+
const hits = manifest.api
|
|
273
|
+
.map((e) => {
|
|
274
|
+
const name = e.name.toLowerCase();
|
|
275
|
+
let score = 0;
|
|
276
|
+
if (name === q)
|
|
277
|
+
score = 100;
|
|
278
|
+
else if (name.startsWith(q))
|
|
279
|
+
score = 50;
|
|
280
|
+
else if (name.includes(q))
|
|
281
|
+
score = 25;
|
|
282
|
+
else if (e.module.includes(q))
|
|
283
|
+
score = 5;
|
|
284
|
+
return { e, score };
|
|
285
|
+
})
|
|
286
|
+
.filter((x) => x.score > 0)
|
|
287
|
+
.sort((a, b) => b.score - a.score || a.e.name.localeCompare(b.e.name))
|
|
288
|
+
.slice(0, limit ?? 20);
|
|
289
|
+
if (hits.length === 0) {
|
|
290
|
+
return { content: [{ type: "text", text: `No exports matched "${query}".` }] };
|
|
291
|
+
}
|
|
292
|
+
const text = hits
|
|
293
|
+
.map(({ e }) => {
|
|
294
|
+
const doc = moduleToDoc(e.module);
|
|
295
|
+
return `- ${e.kind === "type" ? "type " : ""}${e.name} (module: ${e.module}${doc ? `, doc: ${doc}` : ""})`;
|
|
296
|
+
})
|
|
297
|
+
.join("\n");
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: "text",
|
|
302
|
+
text: `Import from "@shaferllc/keel/core":\n\n${text}\n\nRead a module's guide with keel_read_doc.`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
server.registerTool("keel_list_generators", {
|
|
308
|
+
title: "List Keel generators",
|
|
309
|
+
description: "List the `keel make:*` code generators, what each produces, and their flags. Use keel_scaffold to generate one.",
|
|
310
|
+
inputSchema: {},
|
|
311
|
+
}, async () => {
|
|
312
|
+
const text = manifest.generators
|
|
313
|
+
.map((g) => `- ${g.command}\n → ${g.produces}${g.flags.length ? `\n flags: ${g.flags.join(", ")}` : ""}\n ${g.note}`)
|
|
314
|
+
.join("\n\n");
|
|
315
|
+
return { content: [{ type: "text", text: text || "No generator metadata — run `npm run build:ai`." }] };
|
|
316
|
+
});
|
|
317
|
+
server.registerTool("keel_scaffold", {
|
|
318
|
+
title: "Scaffold Keel code",
|
|
319
|
+
description: "Generate the stub for a Keel construct (controller, provider, middleware, factory, seeder, job, notification, transformer) and return the code plus its target path. Does NOT write to disk — write the returned code yourself. Mirrors `keel make:*`.",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
kind: z.enum(GENERATOR_KINDS).describe("What to generate"),
|
|
322
|
+
name: z.string().describe("Base name, e.g. 'User', 'SendWelcome' — suffixes are normalized"),
|
|
323
|
+
resource: z.boolean().optional().describe("controller only: scaffold all seven RESTful actions"),
|
|
324
|
+
model: z.string().optional().describe("transformer only: the model it maps, e.g. 'User'"),
|
|
325
|
+
},
|
|
326
|
+
}, async ({ kind, name, resource, model }) => {
|
|
327
|
+
const { path, code } = scaffold(kind, name, { resource, model });
|
|
328
|
+
return {
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: "text",
|
|
332
|
+
text: `Target: ${path}\n\n\`\`\`ts\n${code}\`\`\`\n\nWrite this file, then register/import it where appropriate (see keel_read_doc { slug: "${moduleToDoc(kind) ?? kind}" }).`,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
// ---- resources ----
|
|
338
|
+
server.registerResource("overview", "keel://overview", { title: "Keel overview", description: "Conventions, layout, topics, generators", mimeType: "text/markdown" }, async (uri) => ({ contents: [{ uri: uri.href, text: overviewText() }] }));
|
|
339
|
+
const llmsFull = join(root, "llms-full.txt");
|
|
340
|
+
if (await exists(llmsFull)) {
|
|
341
|
+
server.registerResource("llms-full", "keel://llms-full", { title: "All Keel docs", description: "Every guide concatenated", mimeType: "text/markdown" }, async (uri) => ({ contents: [{ uri: uri.href, text: await readFile(llmsFull, "utf8") }] }));
|
|
342
|
+
}
|
|
343
|
+
for (const doc of manifest.docs) {
|
|
344
|
+
server.registerResource(`doc:${doc.slug}`, `keel://docs/${doc.slug}`, { title: doc.title, description: doc.summary || doc.title, mimeType: "text/markdown" }, async (uri) => ({ contents: [{ uri: uri.href, text: await readFile(join(root, doc.path), "utf8") }] }));
|
|
345
|
+
}
|
|
346
|
+
return server;
|
|
347
|
+
}
|
|
348
|
+
/** Boot the server on stdio. Called by the `keel-mcp` bin and `keel mcp`. */
|
|
349
|
+
export async function runMcpServer() {
|
|
350
|
+
const server = await createServer();
|
|
351
|
+
const transport = new StdioServerTransport();
|
|
352
|
+
await server.connect(transport);
|
|
353
|
+
// stdout is the protocol channel — log to stderr only.
|
|
354
|
+
console.error("⚓ Keel MCP server running on stdio");
|
|
355
|
+
}
|