@tdk-landscape/tdk-cli-core 1.3.91 ā 1.3.92
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/cli/dist/backend-frameworks/mcp.js +87 -0
- package/cli/dist/backend-frameworks/registry.js +16 -0
- package/cli/dist/commands/completion.js +3 -3
- package/cli/dist/commands/doctor.js +8 -2
- package/cli/dist/commands/networks.js +2 -1
- package/cli/dist/commands/project.js +15 -2
- package/cli/dist/commands/resource.js +19 -13
- package/cli/dist/commands/up.js +37 -1
- package/cli/dist/types/index.js +1 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/doctor-runtime.js +3 -2
- package/cli/dist/utils/doctor-wiring.js +2 -1
- package/cli/dist/utils/env-validator.js +118 -30
- package/cli/dist/utils/resource-kind.js +8 -0
- package/cli/dist/utils/service-manifest.js +23 -3
- package/cli/dist/utils/service-urls.js +4 -3
- package/cli/dist/utils/services.js +2 -1
- package/cli/dist/utils/smoke.js +225 -0
- package/cli/package.json +1 -1
- package/engine/schemas/service-schema.json +73 -27
- package/engine/topologies/platform/docker/auth/auth.star +66 -28
- package/engine/topologies/platform/docker/compose/compose.star +8 -26
- package/engine/topologies/platform/docker/layers/l3_builder_layers.star +3 -0
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +3 -0
- package/engine/topologies/platform/docker/networking/api_path_constants.star +1 -57
- package/engine/topologies/platform/docker/secrets/env_secrets.star +72 -0
- package/engine/topologies/tilt/common/utils.star +0 -2
- package/engine/topologies/tilt/common/utils_env.star +72 -25
- package/engine/topologies/tilt/manifest/constants.star +2 -0
- package/engine/topologies/tilt/manifest/json_to_yaml.py +4 -0
- package/engine/topologies/tilt/manifest/loader.star +7 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/env.star +43 -96
- 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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,7 +110,7 @@ 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
|
}
|
|
@@ -296,7 +297,7 @@ describe('${name}', () => {
|
|
|
296
297
|
export const resourceCommand = new Command("resource")
|
|
297
298
|
.description("Create a new resource (service) from scratch, or register an existing one")
|
|
298
299
|
.argument("[name]", "Resource name (kebab-case)")
|
|
299
|
-
.option("-t, --type <type>", "Resource type: backend, frontend, worker, bring-your-own, sdk", "backend")
|
|
300
|
+
.option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, bring-your-own, sdk", "backend")
|
|
300
301
|
.option("--framework <id>", "Framework: frontend react (default), vue, svelte, preact, lit, solid, qwik, vanilla; backend hono (default), express, elysia, fastify, nestjs, koa, h3")
|
|
301
302
|
.option("--language <id>", "Backend language: bun (default), python, go, rust")
|
|
302
303
|
.option("-s, --stack <stack>", "Stack to assign resource to", "default")
|
|
@@ -346,6 +347,7 @@ export const resourceCommand = new Command("resource")
|
|
|
346
347
|
{ title: "backend - API service with HTTP endpoints", value: "backend" },
|
|
347
348
|
{ title: "frontend - Web application/UI", value: "frontend" },
|
|
348
349
|
{ title: "worker - Background job processor", value: "worker" },
|
|
350
|
+
{ title: "mcp - Model Context Protocol server (HTTP)", value: "mcp" },
|
|
349
351
|
{
|
|
350
352
|
title: "bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
|
|
351
353
|
value: "bring-your-own",
|
|
@@ -361,6 +363,9 @@ export const resourceCommand = new Command("resource")
|
|
|
361
363
|
const frontendFramework = resolveFrontendFramework(resourceType, options.framework);
|
|
362
364
|
const backendLanguage = resolveBackendLanguage(resourceType, options.language);
|
|
363
365
|
const backendFramework = resolveBackendFramework(resourceType, options.framework, backendLanguage);
|
|
366
|
+
// The id passed on to the file generators. An mcp resource has one fixed scaffold and no `framework` setting, so it passes none
|
|
367
|
+
// (its provider is selected by the resource type itself).
|
|
368
|
+
const scaffoldFrameworkId = resourceType === "mcp" ? undefined : (frontendFramework?.id ?? backendFramework?.id);
|
|
364
369
|
let dddEnabled = false;
|
|
365
370
|
if (options.ddd) {
|
|
366
371
|
if (resourceType !== "backend" && resourceType !== "worker") {
|
|
@@ -425,6 +430,7 @@ export const resourceCommand = new Command("resource")
|
|
|
425
430
|
backend: `services/${stackName}/${resourceName}`,
|
|
426
431
|
frontend: `apps/${resourceName}`,
|
|
427
432
|
worker: `workers/${resourceName}`,
|
|
433
|
+
mcp: `services/${stackName}/${resourceName}`,
|
|
428
434
|
"bring-your-own": `services/${stackName}/${resourceName}`,
|
|
429
435
|
sdk: `packages/${resourceName}`,
|
|
430
436
|
};
|
|
@@ -602,8 +608,8 @@ This file contains the resource configuration for TDK.
|
|
|
602
608
|
}
|
|
603
609
|
}
|
|
604
610
|
// Prepare file generation tasks
|
|
605
|
-
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [],
|
|
606
|
-
const packageJson = createPackageJson(resourceName, resourceType,
|
|
611
|
+
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id);
|
|
612
|
+
const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId);
|
|
607
613
|
const tasks = [
|
|
608
614
|
{
|
|
609
615
|
type: "json",
|
|
@@ -622,7 +628,7 @@ This file contains the resource configuration for TDK.
|
|
|
622
628
|
{
|
|
623
629
|
type: "json",
|
|
624
630
|
filename: "tsconfig.json",
|
|
625
|
-
content: createResourceTsconfig(resourceType,
|
|
631
|
+
content: createResourceTsconfig(resourceType, scaffoldFrameworkId),
|
|
626
632
|
description: "Generating tsconfig.json",
|
|
627
633
|
emoji: "āļø",
|
|
628
634
|
},
|
|
@@ -635,12 +641,12 @@ This file contains the resource configuration for TDK.
|
|
|
635
641
|
},
|
|
636
642
|
];
|
|
637
643
|
// Add source files based on resource type
|
|
638
|
-
if (resourceType === "backend") {
|
|
644
|
+
if (resourceType === "backend" || resourceType === "mcp") {
|
|
639
645
|
tasks.push({
|
|
640
646
|
type: "text",
|
|
641
647
|
filename: "src/index.ts",
|
|
642
648
|
content: (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName),
|
|
643
|
-
description: "Generating backend source",
|
|
649
|
+
description: resourceType === "mcp" ? "Generating MCP server source" : "Generating backend source",
|
|
644
650
|
emoji: "š»",
|
|
645
651
|
});
|
|
646
652
|
}
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/cli/dist/types/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
43
|
+
if (isApiServiceType(resource.config?.appType) && typeof port === "number") {
|
|
43
44
|
ports.set(resource.name, port);
|
|
44
45
|
}
|
|
45
46
|
}
|