@apex-stack/core 0.1.8 → 0.1.10

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/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  } from "./chunk-IEXQ7E5C.js";
14
14
 
15
15
  // src/cli.ts
16
- import { resolve as resolve5 } from "path";
17
- import { defineCommand as defineCommand6, runMain } from "citty";
16
+ import { resolve as resolve6 } from "path";
17
+ import { defineCommand as defineCommand7, runMain } from "citty";
18
18
 
19
19
  // src/commands/build.ts
20
20
  import { cpSync, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, rmSync, writeFileSync } from "fs";
@@ -417,15 +417,186 @@ var migrateCommand = defineCommand4({
417
417
  }
418
418
  });
419
419
 
420
- // src/commands/start.ts
421
- import { existsSync as existsSync5 } from "fs";
422
- import { join as join7, resolve as resolve4 } from "path";
420
+ // src/commands/new.ts
421
+ import { spawn, spawnSync } from "child_process";
422
+ import { cpSync as cpSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "fs";
423
+ import { basename, join as join6, resolve as resolve4 } from "path";
424
+ import { fileURLToPath } from "url";
423
425
  import { defineCommand as defineCommand5 } from "citty";
424
426
 
427
+ // src/ui.ts
428
+ var TTY = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== "dumb";
429
+ var RESET = "\x1B[0m";
430
+ function truecolor(r, g, b, s) {
431
+ return TTY ? `\x1B[38;2;${r};${g};${b}m${s}${RESET}` : s;
432
+ }
433
+ function style(code, s) {
434
+ return TTY ? `\x1B[${code}m${s}${RESET}` : s;
435
+ }
436
+ var color = {
437
+ cyan: (s) => truecolor(34, 211, 238, s),
438
+ indigo: (s) => truecolor(129, 140, 248, s),
439
+ green: (s) => truecolor(52, 211, 153, s),
440
+ red: (s) => truecolor(248, 113, 113, s),
441
+ gray: (s) => truecolor(154, 166, 196, s),
442
+ bold: (s) => style("1", s),
443
+ dim: (s) => style("2", s)
444
+ };
445
+ var LOGO = [
446
+ " \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
447
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D",
448
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u255D ",
449
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557 ",
450
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557",
451
+ "\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
452
+ ];
453
+ var FROM = [99, 102, 241];
454
+ var TO = [34, 211, 238];
455
+ function banner(subtitle = "The full-stack, AI-native meta-framework for Alpine.js") {
456
+ const width = LOGO[0].length;
457
+ const rows = LOGO.map((line) => {
458
+ if (!TTY) return ` ${line}`;
459
+ let out = " ";
460
+ for (let i = 0; i < line.length; i++) {
461
+ const t = width > 1 ? i / (width - 1) : 0;
462
+ const r = Math.round(FROM[0] + (TO[0] - FROM[0]) * t);
463
+ const g = Math.round(FROM[1] + (TO[1] - FROM[1]) * t);
464
+ const b = Math.round(FROM[2] + (TO[2] - FROM[2]) * t);
465
+ out += `\x1B[38;2;${r};${g};${b}m${line[i]}`;
466
+ }
467
+ return out + RESET;
468
+ });
469
+ return `
470
+ ${rows.join("\n")}
471
+ ${color.gray(subtitle)}
472
+ `;
473
+ }
474
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
475
+ function spinner(text) {
476
+ if (!TTY) {
477
+ process.stdout.write(` ${text}
478
+ `);
479
+ return {
480
+ succeed: (t) => console.log(` ${color.green("\u2713")} ${t || text}`),
481
+ fail: (t) => console.log(` ${color.red("\u2717")} ${t || text}`),
482
+ stop: () => {
483
+ }
484
+ };
485
+ }
486
+ let i = 0;
487
+ process.stdout.write("\x1B[?25l");
488
+ const id = setInterval(() => {
489
+ process.stdout.write(`\r\x1B[2K ${color.cyan(FRAMES[i++ % FRAMES.length])} ${text}`);
490
+ }, 80);
491
+ const end = (symbol, t) => {
492
+ clearInterval(id);
493
+ process.stdout.write(`\r\x1B[2K ${symbol} ${t || text}
494
+ \x1B[?25h`);
495
+ };
496
+ return {
497
+ succeed: (t) => end(color.green("\u2713"), t),
498
+ fail: (t) => end(color.red("\u2717"), t),
499
+ stop: () => {
500
+ clearInterval(id);
501
+ process.stdout.write("\r\x1B[2K\x1B[?25h");
502
+ }
503
+ };
504
+ }
505
+ function ready(rows) {
506
+ const w = Math.max(...rows.map(([l]) => l.length));
507
+ console.log();
508
+ for (const [label, value] of rows) {
509
+ console.log(` ${color.cyan("\u279C")} ${color.bold(label.padEnd(w))} ${color.cyan(value)}`);
510
+ }
511
+ console.log();
512
+ }
513
+
514
+ // src/commands/new.ts
515
+ var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
516
+ function detectPackageManager() {
517
+ const ua = process.env.npm_config_user_agent || "";
518
+ if (ua.startsWith("pnpm")) return "pnpm";
519
+ if (ua.startsWith("yarn")) return "yarn";
520
+ if (ua.startsWith("bun")) return "bun";
521
+ return "npm";
522
+ }
523
+ function runSync(cmd, args, cwd) {
524
+ return spawnSync(cmd, args, { cwd, stdio: "ignore", shell: process.platform === "win32" }).status === 0;
525
+ }
526
+ function installAsync(pm, cwd) {
527
+ const args = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
528
+ return new Promise((res) => {
529
+ const child = spawn(pm, args, { cwd, stdio: "ignore", shell: process.platform === "win32" });
530
+ child.on("close", (code) => res(code === 0));
531
+ child.on("error", () => res(false));
532
+ });
533
+ }
534
+ var newCommand = defineCommand5({
535
+ meta: { name: "new", description: "Scaffold a new Apex JS app" },
536
+ args: {
537
+ dir: { type: "positional", required: false, description: "Target directory", default: "apex-app" },
538
+ install: { type: "boolean", default: true, description: "Install dependencies (use --no-install to skip)" },
539
+ git: { type: "boolean", default: true, description: "Initialize a git repository (use --no-git to skip)" }
540
+ },
541
+ async run({ args }) {
542
+ const dir = String(args.dir);
543
+ const target = resolve4(process.cwd(), dir);
544
+ const name = basename(target);
545
+ process.stdout.write(banner());
546
+ const log = console.log;
547
+ if (existsSync4(target) && readdirSync3(target).length > 0) {
548
+ console.error(` ${color.red("\u2717")} Target directory is not empty: ${target}
549
+ `);
550
+ process.exit(1);
551
+ }
552
+ cpSync2(TEMPLATE_DIR, target, { recursive: true });
553
+ const gitignore = join6(target, "_gitignore");
554
+ if (existsSync4(gitignore)) renameSync(gitignore, join6(target, ".gitignore"));
555
+ for (const rel of ["package.json", "README.md"]) {
556
+ const file = join6(target, rel);
557
+ if (existsSync4(file)) writeFileSync3(file, readFileSync2(file, "utf8").replaceAll("{{name}}", name));
558
+ }
559
+ log(` ${color.green("\u2713")} Created ${color.bold(dir)}`);
560
+ const pm = detectPackageManager();
561
+ let gitOk = false;
562
+ if (args.git) {
563
+ const hasGit = spawnSync("git", ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
564
+ if (hasGit && runSync("git", ["init", "-q"], target)) {
565
+ runSync("git", ["add", "-A"], target);
566
+ runSync("git", ["commit", "-m", "Initial commit from Apex JS", "--no-gpg-sign"], target);
567
+ gitOk = true;
568
+ }
569
+ }
570
+ if (gitOk) log(` ${color.green("\u2713")} Initialized a git repository`);
571
+ let installed = false;
572
+ if (args.install) {
573
+ const sp = spinner(`Installing dependencies with ${pm}\u2026 ${color.dim("(first run can take a minute)")}`);
574
+ installed = await installAsync(pm, target);
575
+ if (installed) sp.succeed(`Dependencies installed with ${pm}`);
576
+ else sp.fail(`Install failed \u2014 run ${color.cyan(`${pm} install`)} inside ${dir}`);
577
+ }
578
+ const runPrefix = pm === "npm" ? "npm run" : pm;
579
+ log(`
580
+ ${color.bold("Next steps")}`);
581
+ log(` ${color.cyan(`cd ${dir}`)}`);
582
+ if (!installed) log(` ${color.cyan(pm === "yarn" ? "yarn" : `${pm} install`)}`);
583
+ log(` ${color.cyan("apex dev")} ${color.gray("# \u2192 http://localhost:3000")}`);
584
+ log(`
585
+ ${color.gray("Not installed globally? Use")} ${color.cyan(`${runPrefix} dev`)}${color.gray(".")}`);
586
+ log(` ${color.gray("Islands mode:")} ${color.cyan("apex dev --islands")}${color.gray(" \xB7 API routes are also MCP tools at /mcp.")}
587
+ `);
588
+ }
589
+ });
590
+
591
+ // src/commands/start.ts
592
+ import { existsSync as existsSync6 } from "fs";
593
+ import { join as join8, resolve as resolve5 } from "path";
594
+ import { defineCommand as defineCommand6 } from "citty";
595
+
425
596
  // src/prod/server.ts
426
597
  import { createServer as createHttpServer } from "http";
427
- import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "fs";
428
- import { join as join6 } from "path";
598
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
599
+ import { join as join7 } from "path";
429
600
  import { pathToFileURL as pathToFileURL2 } from "url";
430
601
  import {
431
602
  createApp,
@@ -450,8 +621,8 @@ var MIME = {
450
621
  async function startProdServer(options) {
451
622
  const dir = options.dir;
452
623
  const port = options.port ?? 3e3;
453
- const manifest = JSON.parse(readFileSync2(join6(dir, "apex-manifest.json"), "utf8"));
454
- const importServer = (relFile) => import(pathToFileURL2(join6(dir, "server", relFile)).href);
624
+ const manifest = JSON.parse(readFileSync3(join7(dir, "apex-manifest.json"), "utf8"));
625
+ const importServer = (relFile) => import(pathToFileURL2(join7(dir, "server", relFile)).href);
455
626
  const registry = {};
456
627
  let componentCss = "";
457
628
  for (const [name, file] of Object.entries(manifest.components)) {
@@ -473,12 +644,12 @@ async function startProdServer(options) {
473
644
  if (event.method !== "GET") return;
474
645
  const path = decodeURIComponent(getRequestURL(event).pathname);
475
646
  if (path === "/" || path.startsWith("/api") || path === "/mcp") return;
476
- const file = join6(dir, path);
477
- if (!file.startsWith(dir) || !existsSync4(file) || !statSync(file).isFile()) return;
647
+ const file = join7(dir, path);
648
+ if (!file.startsWith(dir) || !existsSync5(file) || !statSync(file).isFile()) return;
478
649
  const ext = path.slice(path.lastIndexOf("."));
479
650
  setResponseHeader(event, "Content-Type", MIME[ext] ?? "application/octet-stream");
480
651
  if (path.startsWith("/assets/")) setResponseHeader(event, "Cache-Control", "public, max-age=31536000, immutable");
481
- return readFileSync2(file);
652
+ return readFileSync3(file);
482
653
  })
483
654
  );
484
655
  if (apiEntries.length) app.use("/api", createApiHandler(apiEntries));
@@ -508,20 +679,20 @@ async function startProdServer(options) {
508
679
  })
509
680
  );
510
681
  const server = createHttpServer(toNodeListener(app));
511
- await new Promise((resolve6) => server.listen(port, resolve6));
682
+ await new Promise((resolve7) => server.listen(port, resolve7));
512
683
  return { server, port };
513
684
  }
514
685
 
515
686
  // src/commands/start.ts
516
- var startCommand = defineCommand5({
687
+ var startCommand = defineCommand6({
517
688
  meta: { name: "start", description: "Run a production build (from `apex build --server`)" },
518
689
  args: {
519
690
  dir: { type: "positional", required: false, description: "Build directory", default: "dist" },
520
691
  port: { type: "string", description: "Port to listen on", default: "3000" }
521
692
  },
522
693
  async run({ args }) {
523
- const dir = resolve4(process.cwd(), args.dir);
524
- if (!existsSync5(join7(dir, "apex-manifest.json"))) {
694
+ const dir = resolve5(process.cwd(), args.dir);
695
+ if (!existsSync6(join8(dir, "apex-manifest.json"))) {
525
696
  console.error(`
526
697
  No build found in ${args.dir}/. Run \`apex build --server\` first.
527
698
  `);
@@ -536,7 +707,7 @@ var startCommand = defineCommand5({
536
707
  });
537
708
 
538
709
  // src/cli.ts
539
- var dev = defineCommand6({
710
+ var dev = defineCommand7({
540
711
  meta: { name: "dev", description: "Start the Apex JS development server" },
541
712
  args: {
542
713
  root: { type: "positional", required: false, description: "Project root", default: "." },
@@ -544,27 +715,60 @@ var dev = defineCommand6({
544
715
  islands: { type: "boolean", description: "Render in islands mode (static-first)", default: false }
545
716
  },
546
717
  async run({ args }) {
547
- const root = resolve5(process.cwd(), args.root);
718
+ const root = resolve6(process.cwd(), args.root);
548
719
  const port = Number(args.port);
549
- const { port: actual } = await startDevServer({ root, port, islands: args.islands });
550
- console.log(`
551
- \x1B[36mApex JS\x1B[0m dev server ready
552
- \u2192 http://localhost:${actual}
553
- `);
720
+ process.stdout.write(banner());
721
+ const sp = spinner(`Starting dev server${args.islands ? " (islands mode)" : ""}\u2026`);
722
+ try {
723
+ const { port: actual } = await startDevServer({ root, port, islands: args.islands });
724
+ sp.succeed("Dev server ready");
725
+ ready([
726
+ ["Local", `http://localhost:${actual}/`],
727
+ ["MCP", `http://localhost:${actual}/mcp`]
728
+ ]);
729
+ } catch (err) {
730
+ sp.fail("Failed to start the dev server");
731
+ throw err;
732
+ }
554
733
  }
555
734
  });
556
- var main = defineCommand6({
735
+ var COMMANDS = [
736
+ ["new", "Scaffold a new app"],
737
+ ["dev", "Start the dev server (SSR + hydrate, API + MCP)"],
738
+ ["build", "Build for production (static, islands, or server)"],
739
+ ["start", "Run a production server build"],
740
+ ["make", "Generate a page, component, or API route"],
741
+ ["migrate", "Apply pending database migrations"],
742
+ ["mcp", "Inspect the MCP server \u2014 list or call tools"]
743
+ ];
744
+ var main = defineCommand7({
557
745
  meta: {
558
746
  name: "apex",
559
747
  description: "The full-stack meta-framework for Alpine.js"
560
748
  },
561
749
  subCommands: {
750
+ new: newCommand,
562
751
  dev,
563
752
  build: buildCommand,
564
753
  start: startCommand,
565
754
  make: makeCommand,
566
755
  migrate: migrateCommand,
567
756
  mcp: mcpCommand
757
+ },
758
+ // Shown for a bare `apex` (no subcommand): the brand banner + a command menu.
759
+ run({ rawArgs }) {
760
+ if (rawArgs.length > 0) return;
761
+ process.stdout.write(banner());
762
+ const log = console.log;
763
+ log(` ${color.bold("Usage")} ${color.gray("apex")} ${color.cyan("<command>")} ${color.gray("[options]")}
764
+ `);
765
+ log(` ${color.bold("Commands")}`);
766
+ for (const [name, desc] of COMMANDS) {
767
+ log(` ${color.cyan(`apex ${name}`.padEnd(13))} ${color.gray(desc)}`);
768
+ }
769
+ log(`
770
+ ${color.gray("Run")} ${color.cyan("apex <command> --help")} ${color.gray("for details.")}
771
+ `);
568
772
  }
569
773
  });
570
774
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-stack/core",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,8 @@
36
36
  }
37
37
  },
38
38
  "files": [
39
- "dist"
39
+ "dist",
40
+ "templates"
40
41
  ],
41
42
  "dependencies": {
42
43
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -54,7 +55,7 @@
54
55
  "node": ">=20.19"
55
56
  },
56
57
  "scripts": {
57
- "build": "tsup",
58
+ "build": "node scripts/copy-templates.mjs && tsup",
58
59
  "dev": "tsup --watch",
59
60
  "typecheck": "tsc --noEmit"
60
61
  }
@@ -0,0 +1,33 @@
1
+ # {{name}}
2
+
3
+ An [Apex JS](https://github.com/andrecorugda/apexjs) app — a meta-framework for
4
+ Alpine.js that renders on the server and hydrates in the browser.
5
+
6
+ ## Getting started
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ Then open [http://localhost:3000](http://localhost:3000).
14
+
15
+ ## Islands mode
16
+
17
+ Ship interactive JavaScript only where you need it:
18
+
19
+ ```bash
20
+ apex dev --islands
21
+ ```
22
+
23
+ ## Project structure
24
+
25
+ - `pages/*.alpine` — single-file components. The `<script server>` block runs on
26
+ the server; its `loader()` return value becomes the Alpine `x-data` scope.
27
+ - `server/api/*.ts` — API routes defined with `defineApexRoute`.
28
+
29
+ ## AI-native API
30
+
31
+ Every route in `server/api/*.ts` is a REST endpoint **and** an MCP tool at the
32
+ same time. Set `mcp: true` on a route (see `server/api/hello.ts`) and it is
33
+ automatically exposed to AI agents at the `/mcp` endpoint — no extra wiring.
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ *.log
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "apex dev",
7
+ "dev:islands": "apex dev --islands"
8
+ },
9
+ "dependencies": {
10
+ "alpinejs": "^3.14.8",
11
+ "@apex-stack/core": "latest",
12
+ "zod": "^4.0.0"
13
+ }
14
+ }
@@ -0,0 +1,107 @@
1
+ <script server lang="ts">
2
+ export function loader() {
3
+ return {
4
+ title: 'Welcome to Apex JS',
5
+ tagline: 'The meta-framework for Alpine.js — server-rendered, then hydrated.',
6
+ }
7
+ }
8
+ </script>
9
+
10
+ <template x-data="{ open: false }">
11
+ <main>
12
+ <h1 x-text="title"></h1>
13
+ <p class="tagline" x-text="tagline"></p>
14
+
15
+ <section class="card">
16
+ <p>
17
+ This page was rendered on the server from
18
+ <code>pages/index.alpine</code>, then hydrated by Alpine in the browser.
19
+ Edit it and save to see your changes live.
20
+ </p>
21
+
22
+ <button type="button" @click="open = !open" x-text="open ? 'Hide the details' : 'Show me how'"></button>
23
+
24
+ <div x-show="open" x-transition class="details">
25
+ <p>
26
+ The <code>loader()</code> in the server block runs on the server and
27
+ hands its data straight to Alpine's <code>x-data</code> scope — no
28
+ fetch, no boilerplate.
29
+ </p>
30
+ <p>
31
+ Next, open <code>server/api/hello.ts</code>: it's a REST endpoint
32
+ <em>and</em> an MCP tool at <code>/mcp</code> at the same time.
33
+ </p>
34
+ </div>
35
+ </section>
36
+
37
+ <p class="hint">
38
+ Run <code>apex dev --islands</code> to ship interactive islands only where
39
+ you need them.
40
+ </p>
41
+ </main>
42
+ </template>
43
+
44
+ <style scoped>
45
+ main {
46
+ max-width: 40rem;
47
+ margin: 4rem auto;
48
+ padding: 0 1.5rem;
49
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
50
+ line-height: 1.6;
51
+ color: #1e293b;
52
+ }
53
+
54
+ h1 {
55
+ color: #2563eb;
56
+ font-size: 2.5rem;
57
+ margin-bottom: 0.5rem;
58
+ }
59
+
60
+ .tagline {
61
+ font-size: 1.15rem;
62
+ color: #475569;
63
+ margin-top: 0;
64
+ }
65
+
66
+ .card {
67
+ margin-top: 2rem;
68
+ padding: 1.5rem;
69
+ border: 1px solid #e2e8f0;
70
+ border-radius: 0.75rem;
71
+ background: #f8fafc;
72
+ }
73
+
74
+ button {
75
+ margin-top: 0.5rem;
76
+ padding: 0.6rem 1.1rem;
77
+ font-size: 1rem;
78
+ font-weight: 600;
79
+ color: #fff;
80
+ background: #2563eb;
81
+ border: none;
82
+ border-radius: 0.5rem;
83
+ cursor: pointer;
84
+ transition: background 0.15s ease;
85
+ }
86
+
87
+ button:hover {
88
+ background: #1d4ed8;
89
+ }
90
+
91
+ .details {
92
+ margin-top: 1rem;
93
+ }
94
+
95
+ code {
96
+ padding: 0.1rem 0.35rem;
97
+ font-size: 0.9em;
98
+ background: #e2e8f0;
99
+ border-radius: 0.35rem;
100
+ }
101
+
102
+ .hint {
103
+ margin-top: 2rem;
104
+ font-size: 0.95rem;
105
+ color: #64748b;
106
+ }
107
+ </style>
@@ -0,0 +1,10 @@
1
+ import { defineApexRoute } from '@apex-stack/core'
2
+ import { z } from 'zod'
3
+
4
+ export default defineApexRoute({
5
+ method: 'GET',
6
+ description: 'Say hello to someone by name',
7
+ input: { name: z.string() },
8
+ mcp: true,
9
+ handler: ({ input }) => ({ message: `Hello, ${input.name}!` }),
10
+ })