@shaferllc/keel 0.66.0 → 0.74.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.
Files changed (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -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
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * OpenAPI configuration. Defaults live here and are merged under
3
+ * `config("openapi")` by the provider; an app overrides them in
4
+ * `config/openapi.ts` (publish it with `keel vendor:publish --tag openapi-config`).
5
+ */
6
+ export interface OpenApiConfig {
7
+ /** Master switch. Off → no docs routes and the spec 404s. */
8
+ enabled: boolean;
9
+ /** URL prefix the docs UI and spec mount under. Default: "docs". */
10
+ path: string;
11
+ /** The API title. Defaults to `config("app.name")`. */
12
+ title: string;
13
+ /** The API version string shown in the spec. */
14
+ version: string;
15
+ /** A longer description (Markdown) for the spec's `info`. */
16
+ description?: string;
17
+ /** Server URLs the API is served from (OpenAPI `servers`). */
18
+ servers: string[];
19
+ /** Serve the docs in production too. Off by default — docs expose your surface. */
20
+ public: boolean;
21
+ /** Swagger UI asset base (a CDN, or your own copy). Pin it to a version you trust. */
22
+ cdn: string;
23
+ /** Route path prefixes to leave out of the spec (the docs' own are always skipped). */
24
+ ignorePaths: string[];
25
+ }
26
+ export declare const defaultConfig: OpenApiConfig;
27
+ /** Read the effective OpenAPI config, filling gaps (title falls back to app name). */
28
+ export declare function resolveConfig(): OpenApiConfig;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenAPI configuration. Defaults live here and are merged under
3
+ * `config("openapi")` by the provider; an app overrides them in
4
+ * `config/openapi.ts` (publish it with `keel vendor:publish --tag openapi-config`).
5
+ */
6
+ import { config } from "../core/helpers.js";
7
+ export const defaultConfig = {
8
+ enabled: true,
9
+ path: "docs",
10
+ title: "",
11
+ version: "1.0.0",
12
+ servers: [],
13
+ public: false,
14
+ cdn: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14",
15
+ ignorePaths: [],
16
+ };
17
+ /** Read the effective OpenAPI config, filling gaps (title falls back to app name). */
18
+ export function resolveConfig() {
19
+ const raw = config("openapi", {});
20
+ return {
21
+ ...defaultConfig,
22
+ ...raw,
23
+ title: raw.title || config("app.name", "API"),
24
+ };
25
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Route documentation metadata. Attach it to a route with `.config(apiDoc(...))`
3
+ * and the generator turns it into a rich OpenAPI operation — request/response
4
+ * schemas, summary, tags. Request schemas reuse Keel's `RequestSchemas` shape, so
5
+ * the very object you hand `validateRequest({ body })` also documents the route.
6
+ *
7
+ * router
8
+ * .post("/users", [Users, "store"])
9
+ * .config(apiDoc({ summary: "Create a user", request: { body: NewUser }, tags: ["users"] }))
10
+ * .middleware([validateRequest({ body: NewUser })]);
11
+ */
12
+ import type { RequestSchemas } from "../core/validation.js";
13
+ /** The key under which docs metadata lives in a route's `config`. */
14
+ export declare const OPENAPI_KEY = "openapi";
15
+ /** One response's documentation: a description and an optional body schema. */
16
+ export interface ResponseDoc {
17
+ description?: string;
18
+ /** A Zod schema or a plain JSON Schema object for the response body. */
19
+ schema?: unknown;
20
+ }
21
+ /** Everything a route can say about itself for the spec. */
22
+ export interface OperationDoc {
23
+ summary?: string;
24
+ description?: string;
25
+ tags?: string[];
26
+ /** Overrides the generated operationId (default: the route name). */
27
+ operationId?: string;
28
+ deprecated?: boolean;
29
+ /** Request body/query/params schemas — Zod or plain JSON Schema. */
30
+ request?: RequestSchemas;
31
+ /** Responses keyed by status code, e.g. `{ 200: { schema: User }, 404: {} }`. */
32
+ responses?: Record<string | number, ResponseDoc>;
33
+ /** Leave this route out of the spec entirely. */
34
+ hidden?: boolean;
35
+ }
36
+ /**
37
+ * Wrap operation docs for a route's `.config()`. Returns `{ openapi: doc }`, which
38
+ * the generator reads off `RouteDefinition.config`.
39
+ */
40
+ export declare function apiDoc(doc: OperationDoc): Record<string, unknown>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Route documentation metadata. Attach it to a route with `.config(apiDoc(...))`
3
+ * and the generator turns it into a rich OpenAPI operation — request/response
4
+ * schemas, summary, tags. Request schemas reuse Keel's `RequestSchemas` shape, so
5
+ * the very object you hand `validateRequest({ body })` also documents the route.
6
+ *
7
+ * router
8
+ * .post("/users", [Users, "store"])
9
+ * .config(apiDoc({ summary: "Create a user", request: { body: NewUser }, tags: ["users"] }))
10
+ * .middleware([validateRequest({ body: NewUser })]);
11
+ */
12
+ /** The key under which docs metadata lives in a route's `config`. */
13
+ export const OPENAPI_KEY = "openapi";
14
+ /**
15
+ * Wrap operation docs for a route's `.config()`. Returns `{ openapi: doc }`, which
16
+ * the generator reads off `RouteDefinition.config`.
17
+ */
18
+ export function apiDoc(doc) {
19
+ return { [OPENAPI_KEY]: doc };
20
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `keel openapi:export [--out openapi.json]` — write the spec to a file, for CI,
3
+ * client generation, or committing it. Contributed as a package command.
4
+ */
5
+ import type { PackageCommand } from "../core/package.js";
6
+ import type { Router } from "../core/http/router.js";
7
+ import type { OpenApiConfig } from "./config.js";
8
+ export declare function exportCommand(getRouter: () => Router, config: OpenApiConfig, base: string): PackageCommand;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `keel openapi:export [--out openapi.json]` — write the spec to a file, for CI,
3
+ * client generation, or committing it. Contributed as a package command.
4
+ */
5
+ import { buildSpec } from "./spec.js";
6
+ export function exportCommand(getRouter, config, base) {
7
+ return {
8
+ name: "openapi:export",
9
+ description: "Write the OpenAPI spec to a file",
10
+ configure: (cmd) => cmd.option("--out <file>", "output path", "openapi.json"),
11
+ action: async (opts) => {
12
+ const spec = buildSpec(getRouter().all(), config, base);
13
+ const out = String(opts.out ?? "openapi.json");
14
+ const { writeFile } = await import("node:fs/promises");
15
+ await writeFile(out, JSON.stringify(spec, null, 2));
16
+ console.log(`✓ Wrote ${out} (${Object.keys(spec.paths).length} paths)`);
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Who may see the docs. A spec advertises your whole API surface, so it's gated
3
+ * shut in production by default — open only when `app.debug` is on, outside
4
+ * production, or when you set `public: true`. Override with `OpenApi.auth()`.
5
+ */
6
+ import type { Ctx } from "../core/http/router.js";
7
+ import type { OpenApiConfig } from "./config.js";
8
+ export type OpenApiGate = (c: Ctx) => boolean | Promise<boolean>;
9
+ export declare const OpenApi: {
10
+ /** Restrict docs access. Return true to allow the request. */
11
+ auth(fn: OpenApiGate): void;
12
+ /** Remove a custom gate, reverting to the default. */
13
+ clearAuth(): void;
14
+ };
15
+ export declare function passesGate(c: Ctx, cfg: OpenApiConfig): Promise<boolean>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Who may see the docs. A spec advertises your whole API surface, so it's gated
3
+ * shut in production by default — open only when `app.debug` is on, outside
4
+ * production, or when you set `public: true`. Override with `OpenApi.auth()`.
5
+ */
6
+ import { config } from "../core/helpers.js";
7
+ let gate;
8
+ export const OpenApi = {
9
+ /** Restrict docs access. Return true to allow the request. */
10
+ auth(fn) {
11
+ gate = fn;
12
+ },
13
+ /** Remove a custom gate, reverting to the default. */
14
+ clearAuth() {
15
+ gate = undefined;
16
+ },
17
+ };
18
+ function defaultGate(cfg) {
19
+ if (cfg.public)
20
+ return true;
21
+ if (config("app.debug", false))
22
+ return true;
23
+ return config("app.env", "production") !== "production";
24
+ }
25
+ export async function passesGate(c, cfg) {
26
+ return gate ? gate(c) : defaultGate(cfg);
27
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Keel OpenAPI — spec generation + Swagger UI for Keel apps, imported from
3
+ * `@shaferllc/keel/openapi`.
4
+ *
5
+ * import { OpenApiServiceProvider } from "@shaferllc/keel/openapi";
6
+ * app.register(OpenApiServiceProvider); // serves /docs and /docs/openapi.json
7
+ *
8
+ * Document a route with `apiDoc()`; lock the docs down with `OpenApi.auth()`.
9
+ */
10
+ export { OpenApiServiceProvider } from "./provider.js";
11
+ export { OpenApi } from "./gate.js";
12
+ export type { OpenApiGate } from "./gate.js";
13
+ export { apiDoc, OPENAPI_KEY } from "./doc.js";
14
+ export type { OperationDoc, ResponseDoc } from "./doc.js";
15
+ export { buildSpec } from "./spec.js";
16
+ export type { OpenApiDocument } from "./spec.js";
17
+ export { toJsonSchema } from "./zod.js";
18
+ export { resolveConfig, defaultConfig } from "./config.js";
19
+ export type { OpenApiConfig } from "./config.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Keel OpenAPI — spec generation + Swagger UI for Keel apps, imported from
3
+ * `@shaferllc/keel/openapi`.
4
+ *
5
+ * import { OpenApiServiceProvider } from "@shaferllc/keel/openapi";
6
+ * app.register(OpenApiServiceProvider); // serves /docs and /docs/openapi.json
7
+ *
8
+ * Document a route with `apiDoc()`; lock the docs down with `OpenApi.auth()`.
9
+ */
10
+ export { OpenApiServiceProvider } from "./provider.js";
11
+ export { OpenApi } from "./gate.js";
12
+ export { apiDoc, OPENAPI_KEY } from "./doc.js";
13
+ export { buildSpec } from "./spec.js";
14
+ export { toJsonSchema } from "./zod.js";
15
+ export { resolveConfig, defaultConfig } from "./config.js";
@@ -0,0 +1,29 @@
1
+ import { env } from "@shaferllc/keel/core";
2
+
3
+ /**
4
+ * Keel OpenAPI — spec + Swagger UI configuration.
5
+ * Published with `keel vendor:publish --tag openapi-config`.
6
+ */
7
+ export default {
8
+ enabled: env("OPENAPI_ENABLED", true),
9
+
10
+ // Docs UI at http://localhost:3000/docs, spec at /docs/openapi.json
11
+ path: "docs",
12
+
13
+ // Shown in the spec's `info`. Title defaults to config("app.name").
14
+ title: "",
15
+ version: "1.0.0",
16
+ description: undefined,
17
+
18
+ // Where the API is served (OpenAPI `servers`), e.g. ["https://api.example.com"].
19
+ servers: [],
20
+
21
+ // Serve the docs in production too. Off by default — a spec advertises your API.
22
+ public: env("OPENAPI_PUBLIC", false),
23
+
24
+ // Swagger UI assets. Pin the version, or point at a copy you host.
25
+ cdn: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14",
26
+
27
+ // Route path prefixes to leave out of the spec.
28
+ ignorePaths: ["/watch"],
29
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Keel OpenAPI — generate an OpenAPI 3 spec from your routes and serve Swagger UI,
3
+ * shipped as a Keel package. One line turns it on:
4
+ *
5
+ * app.register(OpenApiServiceProvider);
6
+ *
7
+ * The spec is built from Keel's own route table plus whatever each route attaches
8
+ * with `.config(apiDoc(...))`. Nothing is scraped or guessed: paths and methods
9
+ * are always right, and schemas are as rich as the metadata a route provides.
10
+ */
11
+ import { PackageProvider } from "../core/package.js";
12
+ export declare class OpenApiServiceProvider extends PackageProvider {
13
+ readonly name = "openapi";
14
+ private config;
15
+ private base;
16
+ register(): void;
17
+ boot(): void;
18
+ }