@tdk-landscape/tdk-cli-core 1.3.91 → 1.3.93

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 (33) hide show
  1. package/cli/dist/backend-frameworks/mcp.js +87 -0
  2. package/cli/dist/backend-frameworks/registry.js +16 -0
  3. package/cli/dist/commands/completion.js +3 -3
  4. package/cli/dist/commands/doctor.js +8 -2
  5. package/cli/dist/commands/networks.js +2 -1
  6. package/cli/dist/commands/project.js +15 -2
  7. package/cli/dist/commands/resource.js +37 -22
  8. package/cli/dist/commands/up.js +37 -1
  9. package/cli/dist/types/index.js +11 -0
  10. package/cli/dist/utils/constants.js +4 -0
  11. package/cli/dist/utils/doctor-runtime.js +3 -2
  12. package/cli/dist/utils/doctor-wiring.js +2 -1
  13. package/cli/dist/utils/env-validator.js +118 -30
  14. package/cli/dist/utils/resource-kind.js +8 -0
  15. package/cli/dist/utils/service-manifest.js +23 -3
  16. package/cli/dist/utils/service-urls.js +4 -3
  17. package/cli/dist/utils/services.js +2 -1
  18. package/cli/dist/utils/smoke.js +225 -0
  19. package/cli/package.json +2 -1
  20. package/engine/schemas/service-schema.json +145 -27
  21. package/engine/topologies/platform/docker/auth/auth.star +66 -28
  22. package/engine/topologies/platform/docker/compose/compose.star +8 -26
  23. package/engine/topologies/platform/docker/layers/l3_builder_layers.star +3 -0
  24. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +3 -0
  25. package/engine/topologies/platform/docker/networking/api_path_constants.star +1 -57
  26. package/engine/topologies/platform/docker/secrets/env_secrets.star +72 -0
  27. package/engine/topologies/tilt/common/utils.star +0 -2
  28. package/engine/topologies/tilt/common/utils_env.star +72 -25
  29. package/engine/topologies/tilt/manifest/constants.star +2 -0
  30. package/engine/topologies/tilt/manifest/json_to_yaml.py +8 -3
  31. package/engine/topologies/tilt/manifest/loader.star +7 -0
  32. package/engine/topologies/tilt/resources/orchestrator/generators/env.star +43 -96
  33. package/package.json +1 -1
@@ -0,0 +1,87 @@
1
+ export function getMcpIndexTemplate(name) {
2
+ return `import { createServer } from 'node:http';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { z } from 'zod';
6
+
7
+ // A new McpServer per request keeps the endpoint stateless, so there is no session to lose when the container restarts.
8
+ function buildServer() {
9
+ const server = new McpServer({ name: '${name}', version: '1.0.0' });
10
+
11
+ // Add your tools, resources and prompts here. This one is a placeholder you can delete.
12
+ server.registerTool(
13
+ 'echo',
14
+ { description: 'Echo a message back', inputSchema: { message: z.string() } },
15
+ async ({ message }) => ({ content: [{ type: 'text', text: message }] })
16
+ );
17
+
18
+ return server;
19
+ }
20
+
21
+ async function readBody(req: import('node:http').IncomingMessage) {
22
+ const chunks: Buffer[] = [];
23
+ for await (const chunk of req) chunks.push(chunk as Buffer);
24
+ const raw = Buffer.concat(chunks).toString('utf8');
25
+ return raw ? JSON.parse(raw) : undefined;
26
+ }
27
+
28
+ const httpServer = createServer(async (req, res) => {
29
+ const path = (req.url ?? '/').split('?')[0];
30
+
31
+ // Health check endpoint (required by TILT_RESOURCE_DEFAULTS.star)
32
+ if (path === '/health') {
33
+ res.setHeader('content-type', 'application/json');
34
+ res.end(JSON.stringify({ status: 'ok', service: '${name}' }));
35
+ return;
36
+ }
37
+
38
+ // MCP over Streamable HTTP. Clients POST JSON-RPC here.
39
+ if (path === '/mcp') {
40
+ if (req.method !== 'POST') {
41
+ res.statusCode = 405;
42
+ res.setHeader('allow', 'POST');
43
+ res.setHeader('content-type', 'application/json');
44
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null }));
45
+ return;
46
+ }
47
+
48
+ const server = buildServer();
49
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
50
+ res.on('close', () => {
51
+ transport.close();
52
+ server.close();
53
+ });
54
+ await server.connect(transport);
55
+ await transport.handleRequest(req, res, await readBody(req));
56
+ return;
57
+ }
58
+
59
+ res.statusCode = 404;
60
+ res.setHeader('content-type', 'application/json');
61
+ res.end(JSON.stringify({ error: 'not_found' }));
62
+ });
63
+
64
+ const port = Number(process.env.PORT || 3000);
65
+
66
+ // Bind beyond loopback: Traefik reaches the container over the Docker network.
67
+ httpServer.listen(port, '0.0.0.0', () => {
68
+ console.log('\\nšŸš€ ${name} MCP server on http://localhost:' + port + '/mcp');
69
+ console.log('šŸ“Š Health check: http://localhost:' + port + '/health\\n');
70
+ });
71
+ `;
72
+ }
73
+ /**
74
+ * The scaffold for \`--type mcp\`. It is not in BACKEND_FRAMEWORKS: \`mcp\` is a resource type of its own, not a \`--framework\`
75
+ * choice for a backend.
76
+ */
77
+ export const mcpBackendProvider = {
78
+ id: "mcp",
79
+ label: "Bun + Model Context Protocol server",
80
+ dependencies: {
81
+ "@modelcontextprotocol/sdk": "^1.32.0",
82
+ zod: "^4.0.0",
83
+ },
84
+ devDependencies: {},
85
+ createIndex: getMcpIndexTemplate,
86
+ };
87
+ //# sourceMappingURL=mcp.js.map
@@ -5,6 +5,7 @@ import { expressBackendProvider } from "./express.js";
5
5
  import { fastifyBackendProvider } from "./fastify.js";
6
6
  import { h3BackendProvider } from "./h3.js";
7
7
  import { koaBackendProvider } from "./koa.js";
8
+ import { mcpBackendProvider } from "./mcp.js";
8
9
  import { nestBackendProvider } from "./nestjs.js";
9
10
  /** Hono is the historical default; omitting --framework keeps the exact historical output. */
10
11
  export const DEFAULT_BACKEND_FRAMEWORK = "hono";
@@ -38,6 +39,21 @@ export function getBackendFramework(frameworkId) {
38
39
  * (the historical Hono scaffold). Python owns its runtime, so a framework cannot be combined with it.
39
40
  */
40
41
  export function resolveBackendFramework(resourceType, frameworkId, language) {
42
+ // `mcp` is a resource type with one fixed scaffold, so a framework choice does not apply to it.
43
+ if (resourceType === "mcp") {
44
+ if (frameworkId !== undefined) {
45
+ throw new TdkError("--framework cannot be combined with --type mcp.", [
46
+ "An mcp resource always scaffolds a Model Context Protocol server",
47
+ "Drop --framework, or use --type backend --framework <id>",
48
+ ]);
49
+ }
50
+ if (language?.createFiles) {
51
+ throw new TdkError(`--language ${language.id} cannot be combined with --type mcp.`, [
52
+ "An mcp resource is a Bun service; drop --language",
53
+ ]);
54
+ }
55
+ return mcpBackendProvider;
56
+ }
41
57
  if (resourceType !== "backend" || frameworkId === undefined)
42
58
  return undefined;
43
59
  if (language?.createFiles) {
@@ -42,7 +42,7 @@ _tdk_completions() {
42
42
  return 0
43
43
  ;;
44
44
  --type)
45
- COMPREPLY=( $(compgen -W "backend frontend worker" -- \${cur}) )
45
+ COMPREPLY=( $(compgen -W "backend frontend worker mcp" -- \${cur}) )
46
46
  return 0
47
47
  ;;
48
48
  --stack)
@@ -94,7 +94,7 @@ _tdk() {
94
94
  ;;
95
95
  resource)
96
96
  _arguments
97
- '--type[Resource type]:type:(backend frontend worker)'
97
+ '--type[Resource type]:type:(backend frontend worker mcp)'
98
98
  '--stack[Assign to stack]:stack:_tdk_stacks'
99
99
  '1:resource name:'
100
100
  ;;
@@ -196,7 +196,7 @@ complete -c tdk -l verbose -d 'Enable verbose output'
196
196
 
197
197
  # Command-specific completions
198
198
  # resource command options
199
- complete -c tdk -n '__fish_seen_subcommand_from resource' -l type -d 'Resource type' -a 'backend frontend worker'
199
+ complete -c tdk -n '__fish_seen_subcommand_from resource' -l type -d 'Resource type' -a 'backend frontend worker mcp'
200
200
  complete -c tdk -n '__fish_seen_subcommand_from resource' -l stack -d 'Assign to stack' -a '(tdk stacks 2>/dev/null | string match -r "^ - " | string replace " - " "")'
201
201
 
202
202
  # resources command options
@@ -15,6 +15,7 @@ import { formatCount } from "../utils/formatting.js";
15
15
  import { getHostPortPlan } from "../utils/host-port-config.js";
16
16
  import { createHostPortPlan } from "../utils/host-port-plan.js";
17
17
  import { findProjectRoot } from "../utils/paths.js";
18
+ import { isApiServiceType } from "../utils/resource-kind.js";
18
19
  import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
19
20
  import { discoverResourcesFromRoot } from "../utils/services.js";
20
21
  import { findOnPath } from "../utils/which.js";
@@ -325,7 +326,7 @@ export async function checkBun(exec = execAsync) {
325
326
  };
326
327
  }
327
328
  const resources = discoverResourcesFromRoot(projectRoot);
328
- const hasGeneratedJsServices = resources.some((resource) => resource.config?.appType === "backend" ||
329
+ const hasGeneratedJsServices = resources.some((resource) => isApiServiceType(resource.config?.appType) ||
329
330
  resource.config?.appType === "frontend" ||
330
331
  resource.config?.appType === "worker");
331
332
  if (!hasGeneratedJsServices) {
@@ -382,10 +383,15 @@ function checkEnvironmentVariables() {
382
383
  fix: "Edit .env and provide values for empty variables",
383
384
  };
384
385
  }
386
+ // `validateEnvFile` also reports non-fatal problems (for example a DATABASE_URL password that differs from DB_PASSWORD). The
387
+ // "no .env yet" notice is not one of them: that case is reported as missing keys above.
388
+ const warnings = validation.warnings.filter((warning) => !warning.startsWith(".env file not found"));
385
389
  return {
386
390
  name: "Environment Variables",
387
391
  didPass: true,
388
- message: "All required environment variables are set",
392
+ message: warnings.length > 0
393
+ ? `All required environment variables are set (warning: ${warnings.join("; ")})`
394
+ : "All required environment variables are set",
389
395
  };
390
396
  }
391
397
  function checkMasterConfigs() {
@@ -11,6 +11,7 @@ import { createHostPortPlan } from "../utils/host-port-plan.js";
11
11
  import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
12
12
  import { findProjectRoot } from "../utils/paths.js";
13
13
  import { checkPortStatus } from "../utils/port-assignment.js";
14
+ import { isApiServiceType } from "../utils/resource-kind.js";
14
15
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
15
16
  import { findOnPath } from "../utils/which.js";
16
17
  function execSafe(command, args, options = {}) {
@@ -181,7 +182,7 @@ export const networksCommand = new Command("networks")
181
182
  .filter((s) => typeof s.config?.basePath === "string")
182
183
  .map(async (s) => {
183
184
  const basePath = s.config.basePath.replace(/^\//, "");
184
- const isBackend = s.config?.appType === "backend";
185
+ const isBackend = isApiServiceType(s.config?.appType);
185
186
  const host = isBackend ? apiDomain : appDomain;
186
187
  const url = `http://${host}:${httpPort}/${basePath}`;
187
188
  const port = s.config.port;
@@ -7,7 +7,7 @@ import chalk from "chalk";
7
7
  import { Command } from "commander";
8
8
  import { generateMasterConfigs, readProjectConfig } from "../generator/template-engine.js";
9
9
  import { MASTER_CONFIG_FILES } from "../utils/constants.js";
10
- import { ensureEnvFile, validateEnvFile } from "../utils/env-validator.js";
10
+ import { completeEnvFile, ensureEnvFile, validateEnvFile } from "../utils/env-validator.js";
11
11
  import { errorFactories, runCommand, showErrorAndExit } from "../utils/errors.js";
12
12
  import { ensureDirectory, ensureGitignore, writeJsonFile } from "../utils/file-helpers.js";
13
13
  import { showCancelled, showCommandHeader, showDetail, showStep, showSuccess, } from "../utils/formatting.js";
@@ -244,6 +244,12 @@ export const projectCommand = new Command("project")
244
244
  if (ensureEnvFile(projectRoot)) {
245
245
  showSuccess("Created: .env (the defaults work as-is)");
246
246
  }
247
+ else {
248
+ const added = completeEnvFile(projectRoot);
249
+ if (added.length > 0) {
250
+ showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
251
+ }
252
+ }
247
253
  const ignored = ensureGitignore(projectRoot);
248
254
  if (ignored.length > 0) {
249
255
  showSuccess(`Added to .gitignore: ${ignored.join(", ")}`);
@@ -387,7 +393,14 @@ export const projectCommand = new Command("project")
387
393
  showSuccess("Created: .env (the defaults work as-is)");
388
394
  }
389
395
  else {
390
- showSuccess("Found: .env (environment already configured)");
396
+ // An .env from an older CLI lacks keys a newer one expects; add them without changing anything already there.
397
+ const added = completeEnvFile(projectRoot);
398
+ if (added.length > 0) {
399
+ showSuccess(`Updated: .env (added ${added.join(", ")}; existing values unchanged)`);
400
+ }
401
+ else {
402
+ showSuccess("Found: .env (environment already configured)");
403
+ }
391
404
  }
392
405
  const envValidation = validateEnvFile(projectRoot);
393
406
  if (envValidation.missing.length > 0) {
@@ -21,7 +21,8 @@ import { discoverResources } from "../utils/services.js";
21
21
  import { createKebabCaseValidator, isPathSafe, validateResourceName } from "../utils/validation.js";
22
22
  export const BASE_TEMPLATE = {
23
23
  port: 0, // Will be assigned
24
- dependencies: [],
24
+ // `dependsOn` is the current name; `dependencies` is deprecated and `tdk doctor` warns about it, so a fresh scaffold must not write it.
25
+ dependsOn: [],
25
26
  build: {
26
27
  dockerfile: "Dockerfile",
27
28
  context: ".",
@@ -33,7 +34,7 @@ export const BASE_TEMPLATE = {
33
34
  };
34
35
  export const TYPE_SPECIFIC = {
35
36
  backend: {
36
- healthCheck: "/health",
37
+ healthCheckPath: "/health",
37
38
  },
38
39
  frontend: {
39
40
  dev: {
@@ -47,8 +48,11 @@ export const TYPE_SPECIFIC = {
47
48
  watch: ["src/**/*"],
48
49
  },
49
50
  },
51
+ mcp: {
52
+ healthCheckPath: "/health",
53
+ },
50
54
  "bring-your-own": {
51
- healthCheck: "/health",
55
+ healthCheckPath: "/health",
52
56
  },
53
57
  };
54
58
  const BYO_RESTART_POLICIES = ["no", "on-failure", "unless-stopped", "always"];
@@ -85,9 +89,6 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
85
89
  }
86
90
  if (language?.devCommand) {
87
91
  base.dev = { command: language.devCommand, watch: language.watch ?? base.dev.watch };
88
- // Provider-owned runtimes declare the probe path the engine reads; Bun keeps its historical key.
89
- base.healthCheckPath = base.healthCheck;
90
- delete base.healthCheck;
91
92
  // The engine generates the image (.autogenerated/Dockerfile.app.autogenerated); a build
92
93
  // block would point at a Dockerfile that does not exist.
93
94
  delete base.build;
@@ -109,10 +110,24 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
109
110
  stack,
110
111
  port,
111
112
  ...(framework ? { framework: framework.id } : {}),
112
- ...(backendFramework ? { framework: backendFramework.id } : {}),
113
+ ...(backendFramework && type !== "mcp" ? { framework: backendFramework.id } : {}),
113
114
  ...(language ? { language: language.id } : {}),
114
115
  };
115
116
  }
117
+ export function createByoServiceJson(name, stack, port, options) {
118
+ return {
119
+ $schema: SERVICE_MANIFEST_SCHEMA_URL,
120
+ schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
121
+ appName: name,
122
+ appType: "bring-your-own",
123
+ stack,
124
+ port,
125
+ healthCheckPath: options.healthCheckPath,
126
+ ...(options.image ? { image: options.image } : { dockerfile: options.dockerfile }),
127
+ ...(options.exposeViaProxy === false ? { exposeViaProxy: false } : {}),
128
+ ...(options.restart ? { restart: options.restart } : {}),
129
+ };
130
+ }
116
131
  export function createPackageJson(name, type, frameworkId) {
117
132
  const isFrontend = type === "frontend";
118
133
  const framework = resolveFrontendFramework(type, frameworkId);
@@ -296,7 +311,7 @@ describe('${name}', () => {
296
311
  export const resourceCommand = new Command("resource")
297
312
  .description("Create a new resource (service) from scratch, or register an existing one")
298
313
  .argument("[name]", "Resource name (kebab-case)")
299
- .option("-t, --type <type>", "Resource type: backend, frontend, worker, bring-your-own, sdk", "backend")
314
+ .option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, bring-your-own, sdk", "backend")
300
315
  .option("--framework <id>", "Framework: frontend react (default), vue, svelte, preact, lit, solid, qwik, vanilla; backend hono (default), express, elysia, fastify, nestjs, koa, h3")
301
316
  .option("--language <id>", "Backend language: bun (default), python, go, rust")
302
317
  .option("-s, --stack <stack>", "Stack to assign resource to", "default")
@@ -346,6 +361,7 @@ export const resourceCommand = new Command("resource")
346
361
  { title: "backend - API service with HTTP endpoints", value: "backend" },
347
362
  { title: "frontend - Web application/UI", value: "frontend" },
348
363
  { title: "worker - Background job processor", value: "worker" },
364
+ { title: "mcp - Model Context Protocol server (HTTP)", value: "mcp" },
349
365
  {
350
366
  title: "bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
351
367
  value: "bring-your-own",
@@ -361,6 +377,9 @@ export const resourceCommand = new Command("resource")
361
377
  const frontendFramework = resolveFrontendFramework(resourceType, options.framework);
362
378
  const backendLanguage = resolveBackendLanguage(resourceType, options.language);
363
379
  const backendFramework = resolveBackendFramework(resourceType, options.framework, backendLanguage);
380
+ // The id passed on to the file generators. An mcp resource has one fixed scaffold and no `framework` setting, so it passes none
381
+ // (its provider is selected by the resource type itself).
382
+ const scaffoldFrameworkId = resourceType === "mcp" ? undefined : (frontendFramework?.id ?? backendFramework?.id);
364
383
  let dddEnabled = false;
365
384
  if (options.ddd) {
366
385
  if (resourceType !== "backend" && resourceType !== "worker") {
@@ -425,6 +444,7 @@ export const resourceCommand = new Command("resource")
425
444
  backend: `services/${stackName}/${resourceName}`,
426
445
  frontend: `apps/${resourceName}`,
427
446
  worker: `workers/${resourceName}`,
447
+ mcp: `services/${stackName}/${resourceName}`,
428
448
  "bring-your-own": `services/${stackName}/${resourceName}`,
429
449
  sdk: `packages/${resourceName}`,
430
450
  };
@@ -528,18 +548,13 @@ export const resourceCommand = new Command("resource")
528
548
  }
529
549
  mkdirSync(fullPath, { recursive: true });
530
550
  // Create service.json
531
- const byoServiceJson = {
532
- $schema: SERVICE_MANIFEST_SCHEMA_URL,
533
- schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
534
- appName: resourceName,
535
- appType: "bring-your-own",
536
- stack: stackName,
537
- port,
551
+ const byoServiceJson = createByoServiceJson(resourceName, stackName, port, {
538
552
  healthCheckPath: options.healthPath || "/health",
539
- ...(options.image ? { image: options.image } : { dockerfile }),
553
+ dockerfile,
554
+ ...(options.image ? { image: options.image } : {}),
540
555
  ...(options.proxy === false ? { exposeViaProxy: false } : {}),
541
556
  ...(options.restart ? { restart: options.restart } : {}),
542
- };
557
+ });
543
558
  const { writeFileSync } = await import("node:fs");
544
559
  writeFileSync(resolve(fullPath, "service.json"), JSON.stringify(byoServiceJson, null, 2));
545
560
  // Create Dockerfile stub if no image provided and dockerfile doesn't exist
@@ -602,8 +617,8 @@ This file contains the resource configuration for TDK.
602
617
  }
603
618
  }
604
619
  // Prepare file generation tasks
605
- const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id ?? backendFramework?.id, options.language === undefined ? undefined : backendLanguage?.id);
606
- const packageJson = createPackageJson(resourceName, resourceType, frontendFramework?.id ?? backendFramework?.id);
620
+ const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id);
621
+ const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId);
607
622
  const tasks = [
608
623
  {
609
624
  type: "json",
@@ -622,7 +637,7 @@ This file contains the resource configuration for TDK.
622
637
  {
623
638
  type: "json",
624
639
  filename: "tsconfig.json",
625
- content: createResourceTsconfig(resourceType, frontendFramework?.id ?? backendFramework?.id),
640
+ content: createResourceTsconfig(resourceType, scaffoldFrameworkId),
626
641
  description: "Generating tsconfig.json",
627
642
  emoji: "āš™ļø",
628
643
  },
@@ -635,12 +650,12 @@ This file contains the resource configuration for TDK.
635
650
  },
636
651
  ];
637
652
  // Add source files based on resource type
638
- if (resourceType === "backend") {
653
+ if (resourceType === "backend" || resourceType === "mcp") {
639
654
  tasks.push({
640
655
  type: "text",
641
656
  filename: "src/index.ts",
642
657
  content: (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName),
643
- description: "Generating backend source",
658
+ description: resourceType === "mcp" ? "Generating MCP server source" : "Generating backend source",
644
659
  emoji: "šŸ’»",
645
660
  });
646
661
  }
@@ -3,14 +3,17 @@ import chalk from "chalk";
3
3
  import { Command } from "commander";
4
4
  import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
5
5
  import { handleDryRun } from "../utils/command-helpers.js";
6
+ import { completeEnvFile } from "../utils/env-validator.js";
6
7
  import { errorFactories, handleTiltFailure, requireProjectRoot, runCommand, showErrorAndExit, withTiltCheck, } from "../utils/errors.js";
7
8
  import { formatCount } from "../utils/formatting.js";
8
9
  import { exportHostPortPlan, getHostPortPlan, writeSavedHostPortPlan, } from "../utils/host-port-config.js";
9
10
  import { formatHostPortPlan } from "../utils/host-port-plan.js";
10
11
  import { findProjectRoot } from "../utils/paths.js";
11
12
  import { findAvailablePort } from "../utils/port-assignment.js";
13
+ import { isApiServiceType } from "../utils/resource-kind.js";
12
14
  import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
13
15
  import { discoverResources, discoverStacks, getResourcesForStack, stackExists, } from "../utils/services.js";
16
+ import { buildSmokePlans, formatSmokeFailure, runSmokePlans } from "../utils/smoke.js";
14
17
  import { buildTiltUpArgs, runTilt } from "../utils/tilt.js";
15
18
  import { stopTiltOnPort } from "../utils/tilt-process.js";
16
19
  import { enableDiscoveredStacks } from "./project.js";
@@ -76,6 +79,14 @@ export const upCommand = new Command("up")
76
79
  inspectDocker: !options.dryRun,
77
80
  });
78
81
  if (!options.dryRun) {
82
+ // An older project's .env predates keys such as JWT_SECRET, which Compose now requires. Add what is missing (never
83
+ // changing an existing value) before anything starts. Only inside a real project, so a stray run never writes a .env.
84
+ if (findProjectRoot()) {
85
+ const addedEnvKeys = completeEnvFile(projectRoot);
86
+ if (addedEnvKeys.length > 0 && !options.quiet) {
87
+ console.log(chalk.gray(`Added ${addedEnvKeys.join(", ")} to .env (generated; existing values unchanged)`));
88
+ }
89
+ }
79
90
  const copiedAssets = ensureProjectRuntimeAssets(projectRoot);
80
91
  if (copiedAssets.length > 0 && options.verbose && !options.quiet) {
81
92
  console.log(chalk.gray(`Refreshed runtime assets: ${copiedAssets.join(", ")}`));
@@ -112,7 +123,7 @@ export const upCommand = new Command("up")
112
123
  });
113
124
  const { appBase, apiBase } = resolveSubdomainBases(hostPortPlan.ingressHttp);
114
125
  const frontends = servicesToStart.filter((s) => s.config?.appType === "frontend");
115
- const backends = servicesToStart.filter((s) => s.config?.appType === "backend");
126
+ const backends = servicesToStart.filter((s) => isApiServiceType(s.config?.appType));
116
127
  if (frontends.length > 0) {
117
128
  console.log(chalk.blue("\nšŸŒ Frontend URLs:"));
118
129
  frontends.forEach((svc) => {
@@ -183,6 +194,10 @@ export const upCommand = new Command("up")
183
194
  quiet: options.quiet,
184
195
  inheritStdio: !options.quiet, // Suppress tilt output in quiet mode
185
196
  });
197
+ // Services that declare `smoke` are checked through the public URL once Tilt is up. A miss stops Tilt (containers stay for
198
+ // inspection; `tdk down` removes them) and fails the command.
199
+ const smokePlans = buildSmokePlans(servicesToStart, hostPortPlan.ingressHttp);
200
+ let smokeFailed = false;
186
201
  let printedSuccess = false;
187
202
  const successOutput = (async () => {
188
203
  const uiReady = await waitForTiltUi(port);
@@ -191,9 +206,30 @@ export const upCommand = new Command("up")
191
206
  console.log(chalk.blue(line));
192
207
  printedSuccess = true;
193
208
  }
209
+ if (uiReady && smokePlans.length > 0) {
210
+ if (!options.quiet) {
211
+ console.log(chalk.gray(`Smoke check: ${smokePlans.map((p) => p.name).join(", ")}`));
212
+ }
213
+ const results = await runSmokePlans(smokePlans);
214
+ for (const smokeResult of results) {
215
+ if (smokeResult.ok) {
216
+ if (!options.quiet)
217
+ console.log(chalk.green(`Smoke check passed: ${smokeResult.name}`));
218
+ }
219
+ else {
220
+ smokeFailed = true;
221
+ console.error(chalk.red(formatSmokeFailure(smokeResult)));
222
+ }
223
+ }
224
+ if (smokeFailed)
225
+ stopTiltOnPort(port);
226
+ }
194
227
  })();
195
228
  const result = await tiltRun;
196
229
  await successOutput;
230
+ if (smokeFailed) {
231
+ process.exit(1);
232
+ }
197
233
  if (result.exitCode !== 0) {
198
234
  handleTiltFailure("up", result.exitCode);
199
235
  }
@@ -1,7 +1,18 @@
1
+ export const RESOURCE_CONFIG_APP_TYPES = [
2
+ "backend",
3
+ "frontend",
4
+ "library",
5
+ "sdk",
6
+ "worker",
7
+ "migrator",
8
+ "mcp",
9
+ "bring-your-own",
10
+ ];
1
11
  export const CREATABLE_RESOURCE_TYPES = [
2
12
  "backend",
3
13
  "frontend",
4
14
  "worker",
15
+ "mcp",
5
16
  "bring-your-own",
6
17
  ];
7
18
  export function isCreatableResourceType(value) {
@@ -10,6 +10,7 @@ export const VALID_RESOURCE_TYPES = [
10
10
  "sdk",
11
11
  "worker",
12
12
  "migrator",
13
+ "mcp",
13
14
  "bring-your-own",
14
15
  ];
15
16
  // Frontend/library/sdk services are served statically or consumed as
@@ -20,6 +21,7 @@ export const REQUIRED_PACKAGE_SCRIPTS = {
20
21
  backend: ["dev", "build", "start"],
21
22
  worker: ["dev", "build", "start"],
22
23
  migrator: ["dev", "build", "start"],
24
+ mcp: ["dev", "build", "start"],
23
25
  frontend: ["dev", "build"],
24
26
  library: ["dev", "build"],
25
27
  sdk: ["dev", "build"],
@@ -38,6 +40,8 @@ export const OPTIONAL_INFRA_SERVICES = [
38
40
  export const PORT_RANGES = {
39
41
  frontend: { base: 3000, min: 3000, max: 3999, range: "3000-3999" },
40
42
  backend: { base: 4000, min: 4000, max: 4999, range: "4000-4999" },
43
+ // An MCP server is an HTTP service, so it shares the backend-compatible range with bring-your-own.
44
+ mcp: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
41
45
  "bring-your-own": { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
42
46
  worker: { base: 6000, min: 6000, max: 6999, range: "6000-6999" },
43
47
  health: { base: 5000, min: 5000, max: 5999, range: "5000-5999" },
@@ -4,6 +4,7 @@ import { createServer } from "node:net";
4
4
  import { join } from "node:path";
5
5
  import { formatCount } from "./formatting.js";
6
6
  import { findProjectRoot } from "./paths.js";
7
+ import { isApiServiceType } from "./resource-kind.js";
7
8
  import { getProjectName } from "./service-urls.js";
8
9
  import { discoverResources, discoverResourcesFromRoot } from "./services.js";
9
10
  import { findOnPath } from "./which.js";
@@ -725,7 +726,7 @@ export function resolveHealthPaths(config) {
725
726
  export function findMissingHealthRoutes(resources) {
726
727
  const missing = [];
727
728
  for (const resource of resources) {
728
- if (resource.config?.appType !== "backend")
729
+ if (!isApiServiceType(resource.config?.appType))
729
730
  continue;
730
731
  const srcDir = join(resource.path, "src");
731
732
  if (!existsSync(srcDir))
@@ -768,7 +769,7 @@ export function checkHealthRoutes(projectRoot = findProjectRoot() ?? process.cwd
768
769
  message: "Could not read services - skipped health route check",
769
770
  };
770
771
  }
771
- const backends = resources.filter((r) => r.config?.appType === "backend");
772
+ const backends = resources.filter((r) => isApiServiceType(r.config?.appType));
772
773
  if (backends.length === 0) {
773
774
  return {
774
775
  name: "Health Routes",
@@ -5,6 +5,7 @@ import { BACKEND_LANGUAGES } from "../backend-languages/registry.js";
5
5
  import { execAsync } from "./exec-async.js";
6
6
  import { formatCount } from "./formatting.js";
7
7
  import { findProjectRoot } from "./paths.js";
8
+ import { isApiServiceType } from "./resource-kind.js";
8
9
  import { discoverResourcesFromRoot } from "./services.js";
9
10
  /**
10
11
  * Checks for wiring mistakes that only show up minutes into `tdk up`: an image
@@ -39,7 +40,7 @@ function backendPorts(resources) {
39
40
  const ports = new Map();
40
41
  for (const resource of resources) {
41
42
  const port = resource.config?.port;
42
- if (resource.config?.appType === "backend" && typeof port === "number") {
43
+ if (isApiServiceType(resource.config?.appType) && typeof port === "number") {
43
44
  ports.set(resource.name, port);
44
45
  }
45
46
  }