kizlo 0.20.2 → 0.22.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 (47) hide show
  1. package/dist/category/index.js +3 -3
  2. package/dist/category/schema.d.ts +1 -1
  3. package/dist/cli/commands/_setup.js +5 -1
  4. package/dist/cli/commands/dev.js +63 -20
  5. package/dist/cli/daemon/config.js +27 -3
  6. package/dist/cli/daemon/generate.js +8 -2
  7. package/dist/cli/daemon/watch.js +11 -3
  8. package/dist/cli/mcp/index.js +46 -0
  9. package/dist/cli/mcp/routes.js +42 -0
  10. package/dist/cli/mcp/schema.js +212 -0
  11. package/dist/cli/mcp/search.js +255 -0
  12. package/dist/cli/mcp/server.js +248 -0
  13. package/dist/cli/mcp/state.js +7 -0
  14. package/dist/cli/mcp/tools.js +116 -0
  15. package/dist/comment/index.js +4 -4
  16. package/dist/config.d.ts +17 -1
  17. package/dist/email/service.js +1 -1
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +2 -2
  20. package/dist/integrations/astro/integration.js +2 -0
  21. package/dist/integrations/nextjs/integration.js +2 -0
  22. package/dist/integrations/node/integration.js +2 -0
  23. package/dist/integrations/tanstack-start/integration.js +2 -0
  24. package/dist/kizlo.js +4 -3
  25. package/dist/menu/utils.js +2 -2
  26. package/dist/package.js +1 -1
  27. package/dist/page/index.js +4 -4
  28. package/dist/page/schema.d.ts +1 -1
  29. package/dist/post/index.js +4 -4
  30. package/dist/post/schema.d.ts +1 -1
  31. package/dist/procedures.d.ts +7 -7
  32. package/dist/seo/index.js +13 -11
  33. package/dist/seo/types.d.ts +4 -4
  34. package/dist/settings/service.d.ts +11 -11
  35. package/dist/settings/service.js +15 -15
  36. package/dist/shared/constants.js +3 -1
  37. package/dist/shared/integration.d.ts +9 -1
  38. package/dist/shared/integration.js +6 -3
  39. package/dist/tag/index.js +3 -3
  40. package/dist/tag/schema.d.ts +1 -1
  41. package/dist/wordpress/endpoint.d.ts +13 -3
  42. package/dist/wordpress/endpoint.js +24 -12
  43. package/dist/wordpress/generate.js +57 -9
  44. package/dist/wordpress/index.d.ts +1 -1
  45. package/dist/wordpress/introspection.js +26 -11
  46. package/dist/wordpress/types.d.ts +16 -2
  47. package/package.json +4 -2
@@ -18,7 +18,7 @@ const CATEGORY_PROCEDURES = {
18
18
  }, async ({ input, context, errors }) => {
19
19
  const identifier = parseIdentifier(input.params.identifier);
20
20
  if (!identifier) throw errors.CATEGORY_NOT_FOUND();
21
- const response = await context.wordpress.taxonomies.category.retrieve({ identifier: String(identifier.value) });
21
+ const response = await context.wordpress.kizlo.taxonomies.category.retrieve({ params: { identifier: String(identifier.value) } });
22
22
  if (response.error) switch (response.error.code) {
23
23
  case "invalid_taxonomy":
24
24
  case "term_not_found":
@@ -41,7 +41,7 @@ const CATEGORY_PROCEDURES = {
41
41
  output: schemaType(CategoryList),
42
42
  errors: LIST_CATEGORY_ERROR_MAP
43
43
  }, async ({ input, context, errors }) => {
44
- const response = await context.wordpress.taxonomies.category.list({
44
+ const response = await context.wordpress.kizlo.taxonomies.category.list({ query: {
45
45
  page: input.query?.page,
46
46
  per_page: input.query?.perPage,
47
47
  search: input.query?.search,
@@ -53,7 +53,7 @@ const CATEGORY_PROCEDURES = {
53
53
  parent: input.query?.parent,
54
54
  post: input.query?.post,
55
55
  slug: normalizeArrayableValue(input.query?.slug)
56
- });
56
+ } });
57
57
  if (response.error) switch (response.error.code) {
58
58
  case "rest_post_invalid_page_number": throw errors.CATEGORY_INVALID_PAGE();
59
59
  case "rest_no_route": throw errors.CATEGORY_NOT_FOUND();
@@ -4,7 +4,7 @@ import * as _kizlo_shared0 from "@kizlo/shared";
4
4
  import z from "zod/v4";
5
5
 
6
6
  //#region src/category/schema.d.ts
7
- type CategoryCustomFields = WP_CustomFields<"taxonomies.category">;
7
+ type CategoryCustomFields = WP_CustomFields<"kizlo.taxonomies.category">;
8
8
  declare const CategoryCustomFieldsSchema: z.ZodType<CategoryCustomFields, CategoryCustomFields>;
9
9
  declare const Category: z.ZodObject<{
10
10
  id: z.ZodNumber;
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_WORDPRESS_TAG, LOCAL_DIR_REL } from "../wp/constants.js";
2
2
  import { composeStop, createStack, dockerHint, dockerStatus } from "../wp/docker.js";
3
3
  import { DEFAULT_ENV_KEYS, availablePackageManagers, detectInvokingPackageManager, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
4
- import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName, stackProject } from "../daemon/config.js";
4
+ import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, DEFAULT_MCP_PORT, resolveStackName, stackProject } from "../daemon/config.js";
5
5
  import { promptFragment, resolvePromptDefault } from "../presets/template.js";
6
6
  import { createAdminAppPassword } from "../wp/bootstrap.js";
7
7
  import { bootstrapDev } from "../wp/dev.js";
@@ -324,6 +324,10 @@ function devConfigFor(cwd) {
324
324
  return {
325
325
  configDir: cwd,
326
326
  project: stackProject(resolveStackName(cwd), "dev"),
327
+ mcp: {
328
+ port: DEFAULT_MCP_PORT,
329
+ portExplicit: false
330
+ },
327
331
  port: DEFAULT_DEV_PORT,
328
332
  portExplicit: false,
329
333
  dbPort: DEFAULT_DEV_DB_PORT,
@@ -2,7 +2,7 @@ import { createStack, dockerHint, dockerStatus, stackStatus } from "../wp/docker
2
2
  import { findConfigDir } from "../wp/utils.js";
3
3
  import { log } from "../daemon/logger.js";
4
4
  import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
5
- import { resolveDevConfig, usesLocalWordPress } from "../daemon/config.js";
5
+ import { resolveDevConfig, resolveMcpConfig, usesLocalWordPress } from "../daemon/config.js";
6
6
  import { bootstrapDev } from "../wp/dev.js";
7
7
  import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
8
8
  import { syncSiteSettings } from "../wp/settings.js";
@@ -10,6 +10,8 @@ import { devStack } from "../wp/stack.js";
10
10
  import { palette } from "../banner.js";
11
11
  import { watchReload } from "../daemon/reload.js";
12
12
  import { startWatcher } from "../daemon/watch.js";
13
+ import { createMcpState } from "../mcp/state.js";
14
+ import { resolveMcpPort, startMcp } from "../mcp/index.js";
13
15
  import { join } from "node:path";
14
16
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15
17
  import { defineCommand } from "citty";
@@ -42,14 +44,16 @@ function note(text) {
42
44
  * Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin URL on
43
45
  * the loopback ("Local") and — when local WordPress is installed on a LAN address rather than
44
46
  * localhost — on the network, so it can be opened from other devices. On a fresh install, also
45
- * show the default wp-admin login.
47
+ * show the default wp-admin login. The MCP row is the address to paste into a harness's own
48
+ * configuration: nothing writes one, because each harness reads a different file.
46
49
  */
47
- function printSummary(info) {
50
+ function printSummary(info, mcpUrl) {
48
51
  const { cyan, dim, reset } = palette();
49
52
  const { port, hostname } = new URL(info.url);
50
53
  const rows = [["WP Local", `http://localhost:${port}/wp-admin`]];
51
54
  if (hostname !== "localhost") rows.push(["WP Network", `${info.url}/wp-admin`]);
52
55
  rows.push(["Database Connection", `127.0.0.1:${info.dbPort}`]);
56
+ if (mcpUrl) rows.push(["MCP", mcpUrl]);
53
57
  const width = Math.max(...rows.map(([label]) => label.length)) + 1;
54
58
  const lines = ["", ...rows.map(([label, value]) => ` ${dim}- ${`${label}:`.padEnd(width)}${reset} ${cyan}${value}${reset}`)];
55
59
  if (info.adminPassword) lines.push("", ` ${dim}admin login:${reset} ${info.username} / ${info.adminPassword}`);
@@ -180,7 +184,7 @@ function stopping(reason) {
180
184
  * and the teardown work against. Shared by the first boot and every reload reboot, so a changed
181
185
  * `dev.port` in `kizlo.config.ts` is re-resolved the same way each time.
182
186
  */
183
- async function prepareStack(cfg) {
187
+ async function prepareStack(cfg, mcpPort) {
184
188
  await removeProjectContainers(cfg.project);
185
189
  const port = await pickStackPort(cfg.port, {
186
190
  fixed: cfg.portExplicit,
@@ -194,7 +198,8 @@ async function prepareStack(cfg) {
194
198
  const ready = {
195
199
  ...cfg,
196
200
  port,
197
- dbPort
201
+ dbPort,
202
+ mcpPort: mcpPort ?? await resolveMcpPort(cfg.mcp)
198
203
  };
199
204
  createStack(devStack(ready));
200
205
  if (port !== cfg.port) note(`Port ${cfg.port} is in use — serving on ${port} instead.`);
@@ -207,7 +212,7 @@ async function prepareStack(cfg) {
207
212
  * one-time Ctrl+C hint. `start` is when the boot began, so the timing spans the whole reboot including
208
213
  * the spinner. The `.env` writes here are why the caller pauses the reload watcher around a reboot.
209
214
  */
210
- async function report(cfg, creds, reloaded, start) {
215
+ async function syncEnv(cfg, creds) {
211
216
  if (creds.appPassword) {
212
217
  const env = updateWpEnv(cfg, creds);
213
218
  note("Updated .env with the new WordPress credentials");
@@ -224,7 +229,16 @@ async function report(cfg, creds, reloaded, start) {
224
229
  if (!sync.ok) note(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
225
230
  }
226
231
  } else if (syncLocalUrl(cfg.configDir, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
227
- printSummary(creds);
232
+ }
233
+ /**
234
+ * Print the summary and the timed ready line, once everything it names is actually up. Split from
235
+ * {@link syncEnv} because the two have opposite ordering needs: the `.env` writes have to land before
236
+ * the watcher starts, since that is where it reads the connection from, while the summary has to wait
237
+ * until after, so the MCP address it prints belongs to a server that exists. `mcpUrl` is absent when
238
+ * MCP is not running, and the row is then left out rather than pointing at nothing.
239
+ */
240
+ function printReady(creds, reloaded, start, mcpUrl) {
241
+ printSummary(creds, mcpUrl);
228
242
  const { green, dim, bold, reset } = palette();
229
243
  const ms = Date.now() - start;
230
244
  const took = ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
@@ -236,9 +250,14 @@ async function report(cfg, creds, reloaded, start) {
236
250
  * then report. The first-boot path only — reboots go through {@link rebootStack}, which also folds the
237
251
  * prepare step under the spinner.
238
252
  */
239
- async function bootAndReport(cfg) {
253
+ async function bootAndSync(cfg) {
240
254
  const start = Date.now();
241
- await report(cfg, await withSpinner("Starting local WordPress", () => bootstrapDev(cfg)), false, start);
255
+ const creds = await withSpinner("Starting local WordPress", () => bootstrapDev(cfg));
256
+ await syncEnv(cfg, creds);
257
+ return {
258
+ creds,
259
+ start
260
+ };
242
261
  }
243
262
  /**
244
263
  * Reboot the whole stack in place after a config/env change: re-resolve the config, recreate the
@@ -247,17 +266,21 @@ async function bootAndReport(cfg) {
247
266
  * the first boot, so this never re-arms. Returns the freshly resolved config (ports may have moved) so
248
267
  * the caller can re-target its watchers.
249
268
  */
250
- async function rebootStack(cwd, label) {
269
+ async function rebootStack(cwd, label, mcpPort) {
251
270
  const start = Date.now();
252
271
  const { ready, creds } = await withSpinner(label, async () => {
253
- const ready$1 = await prepareStack(await resolveDevConfig(cwd));
272
+ const ready$1 = await prepareStack(await resolveDevConfig(cwd), mcpPort);
254
273
  return {
255
274
  ready: ready$1,
256
275
  creds: await bootstrapDev(ready$1)
257
276
  };
258
277
  });
259
- await report(ready, creds, true, start);
260
- return ready;
278
+ await syncEnv(ready, creds);
279
+ return {
280
+ ready,
281
+ creds,
282
+ start
283
+ };
261
284
  }
262
285
  /**
263
286
  * Boot local WordPress and run in the foreground until exit — the path behind bare `kizlo dev`. Arms
@@ -273,20 +296,33 @@ async function startForeground(cfg) {
273
296
  const cwd = process.cwd();
274
297
  let ready = await prepareStack(cfg);
275
298
  const shutdown = await armForegroundTeardown(ready);
276
- await bootAndReport(ready);
299
+ const { creds, start } = await bootAndSync(ready);
277
300
  const stack = {
278
301
  status: () => stackStatus(ready.project),
279
302
  onStopped: () => shutdown()
280
303
  };
281
- let stopContract = await startWatcher(ready.configDir, { stack });
304
+ const mcp = createMcpState();
305
+ const mcpPort = ready.mcpPort;
306
+ let stopContract = await startWatcher(ready.configDir, {
307
+ stack,
308
+ mcp
309
+ });
310
+ const mcpServer = stopContract && mcpPort !== void 0 ? await startMcp(mcp, mcpPort) : void 0;
311
+ if (!stopContract) log.info("Another `kizlo dev` is serving MCP for this project.");
312
+ printReady(creds, false, start, mcpServer?.url);
282
313
  let reloading = false;
283
314
  const reload = async (changed) => {
284
315
  if (reloading) return;
285
316
  reloading = true;
286
317
  try {
287
318
  stopContract?.();
288
- ready = await rebootStack(cwd, `${changed} changed — restarting the session`);
289
- stopContract = await startWatcher(ready.configDir, { stack });
319
+ const rebooted = await rebootStack(cwd, `${changed} changed — restarting the session`, mcpPort);
320
+ ready = rebooted.ready;
321
+ stopContract = await startWatcher(ready.configDir, {
322
+ stack,
323
+ mcp
324
+ });
325
+ printReady(rebooted.creds, true, rebooted.start, mcpServer?.url);
290
326
  } catch (error) {
291
327
  log.error("Failed to reload the dev session:", error);
292
328
  } finally {
@@ -299,6 +335,7 @@ async function startForeground(cfg) {
299
335
  process.on("exit", () => {
300
336
  stopContract?.();
301
337
  stopReload();
338
+ mcpServer?.stop();
302
339
  });
303
340
  setInterval(() => {}, 1 << 30);
304
341
  await new Promise(() => {});
@@ -312,15 +349,20 @@ async function startForeground(cfg) {
312
349
  * persistent watcher keeps the process alive on its own.
313
350
  */
314
351
  async function watchOnly(cwd) {
315
- let stop = await startWatcher(cwd);
316
- if (!stop) return;
352
+ const mcp = createMcpState();
353
+ let stop = await startWatcher(cwd, { mcp });
354
+ if (!stop) {
355
+ log.info("Another `kizlo dev` is serving MCP for this project.");
356
+ return;
357
+ }
358
+ const mcpServer = await startMcp(mcp, await resolveMcpPort(await resolveMcpConfig(cwd)));
317
359
  let reloading = false;
318
360
  const reload = async () => {
319
361
  if (reloading) return;
320
362
  reloading = true;
321
363
  try {
322
364
  stop?.();
323
- stop = await startWatcher(cwd);
365
+ stop = await startWatcher(cwd, { mcp });
324
366
  } finally {
325
367
  setTimeout(() => {
326
368
  reloading = false;
@@ -331,6 +373,7 @@ async function watchOnly(cwd) {
331
373
  const shutdown = () => {
332
374
  stop?.();
333
375
  stopReload();
376
+ mcpServer?.stop();
334
377
  };
335
378
  process.on("exit", shutdown);
336
379
  for (const signal of [
@@ -50,6 +50,7 @@ const testStackSchema = z.object({
50
50
  ]).optional(),
51
51
  command: z.string().optional()
52
52
  });
53
+ const mcpSchema = z.object({ port: z.number().int().positive().optional() });
53
54
  const localSchema = z.union([z.boolean(), z.object({
54
55
  enable: z.boolean().optional(),
55
56
  name: z.string().optional(),
@@ -62,6 +63,7 @@ const configSchema = z.object({
62
63
  dir: dirSchema.optional(),
63
64
  alias: z.string().optional(),
64
65
  local: localSchema.optional(),
66
+ mcp: mcpSchema.optional(),
65
67
  wordpressClientDir: removedKey("`wordpressClientDir` has been removed. Use `dir: { introspection }` instead."),
66
68
  name: removedKey("`name` is no longer a config root key. Move it under `local.name`."),
67
69
  worktrees: removedKey("`worktrees` is no longer a config root key. Move it under `local.worktrees`."),
@@ -106,6 +108,16 @@ const CONFIG_FILES = [
106
108
  ];
107
109
  const DEFAULT_DEV_PORT = 8080;
108
110
  const DEFAULT_DEV_DB_PORT = 3307;
111
+ /**
112
+ * Port the MCP server binds on `127.0.0.1` when `mcp.port` is unset. It steps off a collision like the
113
+ * stack ports do, so two projects need no configuration to run side by side. A harness config names a
114
+ * port and cannot follow one that moves, which is what setting `mcp.port` is for.
115
+ *
116
+ * Deliberately clear of the stack ports' own scan ranges. Each default steps up to `PORT_SCAN_RANGE`
117
+ * ports looking for a free one, so a default inside 8080-8179 could be handed to the dev stack and
118
+ * then claimed again here, since the MCP probe runs before compose has published anything.
119
+ */
120
+ const DEFAULT_MCP_PORT = 8300;
109
121
  const DEFAULT_TEST_PORT = 8889;
110
122
  /** The generated introspection artifact's filename, written under its resolved directory. */
111
123
  const INTROSPECTION_FILE = "introspection.ts";
@@ -290,6 +302,16 @@ async function resolveWatchCredentials(cwd) {
290
302
  if (!wordPressConnectionComplete(env)) return void 0;
291
303
  return resolveWordPressConnection(env).credentials;
292
304
  }
305
+ function resolveMcp(fileConfig) {
306
+ return {
307
+ port: fileConfig?.mcp?.port ?? DEFAULT_MCP_PORT,
308
+ portExplicit: fileConfig?.mcp?.port !== void 0
309
+ };
310
+ }
311
+ /** Resolve `mcp` from `kizlo.config.*`, applying the defaults for a project that says nothing about it. */
312
+ async function resolveMcpConfig(cwd) {
313
+ return resolveMcp(await loadConfigFile(findConfigDir(cwd)));
314
+ }
293
315
  /**
294
316
  * Resolve the dev stack (`local.dev`) into concrete values for the `dev` command, applying defaults
295
317
  * (port 8080). The install folder is fixed at `.kizlo/local` (no longer a config choice), so there's
@@ -298,7 +320,8 @@ async function resolveWatchCredentials(cwd) {
298
320
  */
299
321
  async function resolveDevConfig(cwd) {
300
322
  const configDir = findConfigDir(cwd);
301
- const local = resolveLocal(await loadConfigFile(configDir));
323
+ const fileConfig = await loadConfigFile(configDir);
324
+ const local = resolveLocal(fileConfig);
302
325
  const { dev } = local;
303
326
  return {
304
327
  configDir,
@@ -313,9 +336,10 @@ async function resolveDevConfig(cwd) {
313
336
  wordpressTag: dev.version ?? DEFAULT_WORDPRESS_TAG,
314
337
  fixtures: dev.fixtures ?? [],
315
338
  wordpressPath: LOCAL_DIR_REL,
316
- wordpressDir: path.resolve(configDir, LOCAL_DIR_REL)
339
+ wordpressDir: path.resolve(configDir, LOCAL_DIR_REL),
340
+ mcp: resolveMcp(fileConfig)
317
341
  };
318
342
  }
319
343
 
320
344
  //#endregion
321
- export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, resolveWatchCredentials, stackProject, usesLocalWordPress };
345
+ export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, DEFAULT_MCP_PORT, resolveConfig, resolveDevConfig, resolveMcpConfig, resolveStackName, resolveTestConfig, resolveWatchCredentials, stackProject, usesLocalWordPress };
@@ -178,7 +178,7 @@ function reportPluginVersion(installed) {
178
178
  async function fetchDocument(cwd, options, etag) {
179
179
  loadEnvFiles(cwd);
180
180
  const result = await fetchIntrospection(options.credentials ?? resolveWordPressConnection(integrationEnv([node()])).credentials, {
181
- etag: options.strict ? void 0 : etag,
181
+ etag: !options.strict && !options.seed ? etag : void 0,
182
182
  fetch: options.fetch
183
183
  });
184
184
  reportPluginVersion(result.pluginVersion);
@@ -208,8 +208,14 @@ async function generateIntrospectionOnce(cfg, options = {}) {
208
208
  if (options.skipIntrospection) return "skipped";
209
209
  const file = path.resolve(cfg.cwd, cfg.introspectionPath);
210
210
  const metaPath = path.resolve(cfg.cwd, cfg.introspectionMetaPath);
211
- const result = await fetchDocument(cfg.cwd, options, readWordPressMeta(metaPath, file)?.etag);
211
+ const meta = readWordPressMeta(metaPath, file);
212
+ const result = await fetchDocument(cfg.cwd, options, meta?.etag);
212
213
  if (!result?.document) return "unchanged";
214
+ options.onDocument?.(result.document);
215
+ if (options.seed && meta && result.document.hash === meta.hash) {
216
+ if (result.etag !== meta.etag) atomicWrite(metaPath, wordPressMeta(result.etag, result.document.hash));
217
+ return "unchanged";
218
+ }
213
219
  const generated = generateWordPressModule(result.document);
214
220
  await assertGeneratedClientCompiles(generated);
215
221
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -103,8 +103,8 @@ function createWordPressRefresh(cfg, options, stack) {
103
103
  };
104
104
  }
105
105
  /** Run {@link createWordPressRefresh} on a timer, skipping a tick while the previous one is still going. */
106
- function refreshWordPress(cfg, credentials, stack) {
107
- const refresh = createWordPressRefresh(cfg, { credentials }, stack);
106
+ function refreshWordPress(cfg, options, stack) {
107
+ const refresh = createWordPressRefresh(cfg, options, stack);
108
108
  let refreshing = false;
109
109
  const timer = setInterval(() => {
110
110
  if (refreshing) return;
@@ -139,6 +139,14 @@ async function startWatcher(cwd, opts) {
139
139
  const credentials = await resolveWatchCredentials(cwd);
140
140
  const options = credentials ? { credentials } : { skipIntrospection: true };
141
141
  if (!credentials) log.info("No WordPress connection configured; running the contract watcher only, introspection skipped.");
142
+ if (opts?.mcp) {
143
+ opts.mcp.credentials = credentials;
144
+ options.seed = true;
145
+ options.onDocument = (document) => {
146
+ if (opts.mcp) opts.mcp.document = document;
147
+ options.seed = false;
148
+ };
149
+ }
142
150
  try {
143
151
  const result = await generateOnce(cfg, options);
144
152
  if (result.contract === "built") log.success("Contract generated");
@@ -148,7 +156,7 @@ async function startWatcher(cwd, opts) {
148
156
  reportGenerationError("Failed to generate the Kizlo contract:", error);
149
157
  }
150
158
  const watcher = cfg.server ? await watch(cfg, cfg.server, options) : void 0;
151
- const wordpressRefresh = credentials ? refreshWordPress(cfg, credentials, opts?.stack) : void 0;
159
+ const wordpressRefresh = credentials ? refreshWordPress(cfg, options, opts?.stack) : void 0;
152
160
  let stopped = false;
153
161
  handedOver = true;
154
162
  return () => {
@@ -0,0 +1,46 @@
1
+ import { log } from "../daemon/logger.js";
2
+ import { pickStackPort } from "../utils.js";
3
+ import { MCP_HOST, MCP_PATH, isAllowedOrigin, startMcpServer } from "./server.js";
4
+ import { createMcpState } from "./state.js";
5
+
6
+ //#region src/cli/mcp/index.ts
7
+ /**
8
+ * The port the server will bind, stepping off a collision unless the project pinned one.
9
+ *
10
+ * Resolved before the server starts so the dev summary can print the address alongside WordPress's.
11
+ * A pinned port that is taken stops the command the way `dev.port` and `test.port` do: the developer
12
+ * wrote that number, a harness config elsewhere names it, and quietly serving on a different one
13
+ * would leave the harness connecting to nothing.
14
+ */
15
+ async function resolveMcpPort(config) {
16
+ return pickStackPort(config.port, {
17
+ fixed: config.portExplicit,
18
+ host: MCP_HOST,
19
+ configKey: "mcp.port"
20
+ });
21
+ }
22
+ /**
23
+ * Start the MCP server for a session that has a watcher, or say why it is not running.
24
+ *
25
+ * Called after the watcher starts and never inside its restart cycle. Two cases leave it off, and
26
+ * neither is a failure of the session: another `kizlo dev` already holds the watcher lock and is
27
+ * serving MCP itself (the caller skips this entirely), or there is no WordPress connection to expose,
28
+ * where the contract watcher still has work to do and MCP has nothing to answer with.
29
+ */
30
+ async function startMcp(state, port) {
31
+ if (!state.credentials) {
32
+ log.info("No WordPress connection configured; the MCP server is off for this session.");
33
+ return;
34
+ }
35
+ try {
36
+ const handle = await startMcpServer(state, { port });
37
+ log.success(`MCP server on ${handle.url}`);
38
+ return handle;
39
+ } catch (error) {
40
+ log.error("Failed to start the MCP server:", error);
41
+ return;
42
+ }
43
+ }
44
+
45
+ //#endregion
46
+ export { resolveMcpPort, startMcp };
@@ -0,0 +1,42 @@
1
+ import { camel, endpointDefinition } from "../../wordpress/generate.js";
2
+
3
+ //#region src/cli/mcp/routes.ts
4
+ /**
5
+ * Every operation in the document, keyed by route name.
6
+ *
7
+ * Built per call rather than cached: the watcher replaces the document whenever WordPress moves, and
8
+ * a registry that outlived one would answer for routes that no longer exist while missing the ones
9
+ * that just appeared. Walking a contract of a few hundred routes is far cheaper than getting that wrong.
10
+ */
11
+ function routesOf(document) {
12
+ const routes = /* @__PURE__ */ new Map();
13
+ for (const [apiId, api] of Object.entries(document.apis)) {
14
+ const prefix = apiId.split(".").map(camel);
15
+ for (const [path, operations] of Object.entries(api.paths)) for (const [operationId, operation] of Object.entries(operations)) {
16
+ const name = [...prefix, camel(operationId)].join(".");
17
+ routes.set(name, {
18
+ name,
19
+ namespace: api.namespace,
20
+ method: operation.method,
21
+ path,
22
+ ...operation.summary !== void 0 ? { summary: operation.summary } : {},
23
+ ...operation.description !== void 0 ? { description: operation.description } : {},
24
+ ...operation.deprecated !== void 0 ? { deprecated: operation.deprecated } : {},
25
+ operation,
26
+ definition: endpointDefinition({
27
+ namespace: api.namespace,
28
+ path,
29
+ operation
30
+ })
31
+ });
32
+ }
33
+ }
34
+ return routes;
35
+ }
36
+ /** Route names in the order a listing shows them, so two calls against one document agree. */
37
+ function sortRoutes(routes) {
38
+ return [...routes].sort((left, right) => left.name.localeCompare(right.name));
39
+ }
40
+
41
+ //#endregion
42
+ export { routesOf, sortRoutes };