@cyanheads/mcp-ts-core 0.9.0 → 0.9.2

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 (95) hide show
  1. package/CLAUDE.md +2 -1
  2. package/README.md +6 -2
  3. package/changelog/0.9.x/0.9.1.md +41 -0
  4. package/changelog/0.9.x/0.9.2.md +55 -0
  5. package/dist/cli/init.js +1 -1
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/core/app.d.ts.map +1 -1
  8. package/dist/core/app.js +3 -0
  9. package/dist/core/app.js.map +1 -1
  10. package/dist/core/context.d.ts +6 -0
  11. package/dist/core/context.d.ts.map +1 -1
  12. package/dist/core/context.js +2 -0
  13. package/dist/core/context.js.map +1 -1
  14. package/dist/core/serverManifest.d.ts +8 -2
  15. package/dist/core/serverManifest.d.ts.map +1 -1
  16. package/dist/core/serverManifest.js +16 -2
  17. package/dist/core/serverManifest.js.map +1 -1
  18. package/dist/linter/rules/format-parity-rules.d.ts.map +1 -1
  19. package/dist/linter/rules/format-parity-rules.js +134 -106
  20. package/dist/linter/rules/format-parity-rules.js.map +1 -1
  21. package/dist/logs/combined.log +7 -7
  22. package/dist/logs/error.log +5 -5
  23. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  24. package/dist/mcp-server/resources/resource-registration.js +2 -0
  25. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  26. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +2 -0
  27. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  28. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +2 -0
  29. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  30. package/dist/mcp-server/server.d.ts +7 -0
  31. package/dist/mcp-server/server.d.ts.map +1 -1
  32. package/dist/mcp-server/server.js +11 -7
  33. package/dist/mcp-server/server.js.map +1 -1
  34. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  35. package/dist/mcp-server/tools/tool-registration.js +4 -0
  36. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  37. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +2 -0
  38. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  39. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +2 -0
  40. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  41. package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
  42. package/dist/mcp-server/transports/http/httpTransport.js +9 -0
  43. package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
  44. package/dist/mcp-server/transports/http/landing-page/assets/styles.d.ts.map +1 -1
  45. package/dist/mcp-server/transports/http/landing-page/assets/styles.js +90 -72
  46. package/dist/mcp-server/transports/http/landing-page/assets/styles.js.map +1 -1
  47. package/dist/mcp-server/transports/http/landing-page/sections/connect.d.ts +6 -4
  48. package/dist/mcp-server/transports/http/landing-page/sections/connect.d.ts.map +1 -1
  49. package/dist/mcp-server/transports/http/landing-page/sections/connect.js +76 -19
  50. package/dist/mcp-server/transports/http/landing-page/sections/connect.js.map +1 -1
  51. package/dist/mcp-server/transports/http/landing-page/sections/head.d.ts +2 -1
  52. package/dist/mcp-server/transports/http/landing-page/sections/head.d.ts.map +1 -1
  53. package/dist/mcp-server/transports/http/landing-page/sections/head.js +20 -2
  54. package/dist/mcp-server/transports/http/landing-page/sections/head.js.map +1 -1
  55. package/dist/mcp-server/transports/http/landing-page/sections/prompts.js +1 -1
  56. package/dist/mcp-server/transports/http/landing-page/sections/prompts.js.map +1 -1
  57. package/dist/mcp-server/transports/http/landing-page/sections/resources.js +1 -1
  58. package/dist/mcp-server/transports/http/landing-page/sections/resources.js.map +1 -1
  59. package/dist/mcp-server/transports/http/landing-page/sections/tools.js +23 -16
  60. package/dist/mcp-server/transports/http/landing-page/sections/tools.js.map +1 -1
  61. package/dist/mcp-server/transports/http/robotsTxt.d.ts +24 -0
  62. package/dist/mcp-server/transports/http/robotsTxt.d.ts.map +1 -0
  63. package/dist/mcp-server/transports/http/robotsTxt.js +39 -0
  64. package/dist/mcp-server/transports/http/robotsTxt.js.map +1 -0
  65. package/dist/testing/fuzz.js +1 -1
  66. package/dist/testing/fuzz.js.map +1 -1
  67. package/dist/testing/index.d.ts +4 -0
  68. package/dist/testing/index.d.ts.map +1 -1
  69. package/dist/testing/index.js +2 -0
  70. package/dist/testing/index.js.map +1 -1
  71. package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
  72. package/dist/utils/telemetry/instrumentation.js +5 -6
  73. package/dist/utils/telemetry/instrumentation.js.map +1 -1
  74. package/package.json +40 -35
  75. package/scripts/devcheck.ts +35 -4
  76. package/scripts/lint-packaging.ts +116 -0
  77. package/scripts/list-skills.ts +170 -0
  78. package/skills/api-workers/SKILL.md +15 -1
  79. package/skills/field-test/SKILL.md +96 -90
  80. package/skills/maintenance/SKILL.md +3 -1
  81. package/skills/multi-server-orchestration/SKILL.md +123 -0
  82. package/skills/multi-server-orchestration/references/greenfield-buildout.md +215 -0
  83. package/skills/multi-server-orchestration/references/maintenance-pass.md +119 -0
  84. package/skills/multi-server-orchestration/references/release-pass.md +189 -0
  85. package/skills/polish-docs-meta/SKILL.md +1 -1
  86. package/skills/polish-docs-meta/references/package-meta.md +1 -1
  87. package/skills/polish-docs-meta/references/readme.md +10 -7
  88. package/skills/polish-docs-meta/references/server-json.md +2 -2
  89. package/skills/release-and-publish/SKILL.md +38 -7
  90. package/skills/setup/SKILL.md +1 -1
  91. package/templates/AGENTS.md +37 -0
  92. package/templates/CLAUDE.md +37 -0
  93. package/templates/_.mcpbignore +13 -0
  94. package/templates/manifest.json +26 -0
  95. package/templates/package.json +6 -1
@@ -416,6 +416,20 @@ const ALL_CHECKS: Check[] = [
416
416
  tip: (c) =>
417
417
  `Fix definition errors above — each diagnostic links to its rule in ${c.bold('skills/api-linter/SKILL.md')}.`,
418
418
  },
419
+ {
420
+ name: 'Packaging',
421
+ flag: '--no-packaging',
422
+ canFix: false,
423
+ // Validates env var alignment between manifest.json (MCPB bundle) and
424
+ // server.json (MCP Registry). Skipped cleanly when manifest.json is absent
425
+ // — consumers who deleted it for an HTTP-only deploy are unaffected.
426
+ getCommand: () => {
427
+ if (!existsSync(path.join(ROOT_DIR, 'manifest.json'))) return null;
428
+ return ['bun', 'run', 'scripts/lint-packaging.ts'];
429
+ },
430
+ tip: (c) =>
431
+ `Align env var names between ${c.bold('manifest.json')} ${c.bold('mcp_config.env')} and ${c.bold('server.json')} stdio package ${c.bold('environmentVariables[]')}.`,
432
+ },
419
433
  {
420
434
  name: 'Framework Antipatterns',
421
435
  flag: '--no-framework-antipatterns',
@@ -591,20 +605,24 @@ const ALL_CHECKS: Check[] = [
591
605
  const output = result.stdout.trim();
592
606
  if (result.exitCode !== 0 && !output.includes('|')) return false;
593
607
 
594
- // Parse the tabular output. Package lines contain '|' separators.
595
- // Filter out header/separator rows and allowlisted packages.
608
+ // Parse the tabular output. `bun outdated` emits markdown-style rows
609
+ // (`| col1 | col2 | ... |`), so split('|') yields an empty leading cell —
610
+ // package data starts at index [1]. Strip the trailing `(dev|peer|prod|optional)`
611
+ // workspace-type marker so the allowlist takes the bare package name.
596
612
  const lines = output.split('\n');
613
+ const stripWorkspaceMarker = (cell: string): string =>
614
+ cell.replace(/\s*\((?:dev|peer|prod|optional)\)$/, '');
597
615
  const packageLines = lines.filter((line) => {
598
616
  if (!line.includes('|')) return false;
599
617
  // Skip table chrome: header row and separator (e.g., "---")
600
- const firstCell = line.split('|')[0]?.trim() ?? '';
618
+ const firstCell = line.split('|')[1]?.trim() ?? '';
601
619
  if (!firstCell || firstCell === 'Package' || /^-+$/.test(firstCell)) return false;
602
620
  return true;
603
621
  });
604
622
 
605
623
  // Check if every outdated package is in the allowlist
606
624
  const unexpected = packageLines.filter((line) => {
607
- const pkgName = line.split('|')[0]?.trim() ?? '';
625
+ const pkgName = stripWorkspaceMarker(line.split('|')[1]?.trim() ?? '');
608
626
  return !OUTDATED_ALLOWLIST.has(pkgName);
609
627
  });
610
628
 
@@ -915,6 +933,19 @@ async function runCheck(check: Check, ctx: AppContext): Promise<CommandResult> {
915
933
  const result = await Shell.exec(command, { cwd: ctx.rootDir });
916
934
  const duration = Math.round(performance.now() - startTime);
917
935
 
936
+ // Bun's node-shim (via `bun run`) emits "Registry URL must be" errors when
937
+ // depcheck encounters `cloudflare:*` virtual-module specifiers in Workers
938
+ // tests. depcheck.ignores already filters them from the report — strip the
939
+ // cosmetic stderr so the summary stays clean.
940
+ if (name === 'Unused Dependencies' && result.stderr) {
941
+ result.stderr = result.stderr
942
+ .replace(
943
+ /error: Registry URL must be http:\/\/ or https:\/\/\nReceived: "cloudflare:[^"]*"\n?/g,
944
+ '',
945
+ )
946
+ .trim();
947
+ }
948
+
918
949
  const finalResult: CommandResult = {
919
950
  ...baseResult,
920
951
  ...result,
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview MCPB packaging linter — validates env var alignment between
4
+ * `manifest.json` (MCPB bundle install UX) and `server.json` (MCP Registry
5
+ * discovery) for stdio packages.
6
+ *
7
+ * Used by devcheck and as a standalone script: `bun run lint:packaging` /
8
+ * `npm run lint:packaging`.
9
+ *
10
+ * Checks:
11
+ * 1. Every `${user_config.X}` reference in manifest `mcp_config.env` must
12
+ * appear in server.json stdio `environmentVariables[]` (the registry
13
+ * advertises the configurable knob the bundle surfaces).
14
+ * 2. Every required stdio env var in server.json (no default) must appear
15
+ * as a key in manifest `mcp_config.env` (the bundle can receive it).
16
+ *
17
+ * Skips cleanly when `manifest.json` is absent — consumers who deleted it for
18
+ * an HTTP-only deploy should not fail this check.
19
+ *
20
+ * @module scripts/lint-packaging
21
+ */
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { resolve } from 'node:path';
24
+
25
+ interface ServerJsonEnvVar {
26
+ default?: string;
27
+ isRequired?: boolean;
28
+ name: string;
29
+ }
30
+
31
+ interface ServerJsonPackage {
32
+ environmentVariables?: ServerJsonEnvVar[];
33
+ transport?: { type?: string };
34
+ }
35
+
36
+ interface ServerJson {
37
+ packages?: ServerJsonPackage[];
38
+ }
39
+
40
+ interface Manifest {
41
+ server?: { mcp_config?: { env?: Record<string, string> } };
42
+ user_config?: Record<string, unknown>;
43
+ }
44
+
45
+ const USER_CONFIG_REF = /^\$\{user_config\.([\w-]+)\}$/;
46
+
47
+ function tryReadJson<T>(path: string): T | undefined {
48
+ try {
49
+ if (!existsSync(path)) return;
50
+ return JSON.parse(readFileSync(path, 'utf-8')) as T;
51
+ } catch (err) {
52
+ console.error(`Failed to parse ${path}: ${err instanceof Error ? err.message : err}`);
53
+ return;
54
+ }
55
+ }
56
+
57
+ function main(): void {
58
+ const manifestPath = resolve('manifest.json');
59
+ if (!existsSync(manifestPath)) {
60
+ console.log('No manifest.json — skipping lint:packaging.');
61
+ process.exit(0);
62
+ }
63
+
64
+ const manifest = tryReadJson<Manifest>(manifestPath);
65
+ if (!manifest) {
66
+ console.error('manifest.json is unreadable or malformed.');
67
+ process.exit(1);
68
+ }
69
+
70
+ const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
71
+ if (!serverJson) {
72
+ console.log('No server.json — skipping cross-validation.');
73
+ process.exit(0);
74
+ }
75
+
76
+ const manifestEnv = manifest.server?.mcp_config?.env ?? {};
77
+ const manifestEnvKeys = new Set(Object.keys(manifestEnv));
78
+
79
+ const manifestUserConfigKeys = new Set(
80
+ Object.entries(manifestEnv)
81
+ .filter(([, v]) => typeof v === 'string' && USER_CONFIG_REF.test(v))
82
+ .map(([k]) => k),
83
+ );
84
+
85
+ const stdioEnvVars = (serverJson.packages ?? [])
86
+ .filter((p) => p.transport?.type === 'stdio')
87
+ .flatMap((p) => p.environmentVariables ?? []);
88
+ const stdioEnvNames = new Set(stdioEnvVars.map((v) => v.name));
89
+ const requiredStdioEnvNames = new Set(
90
+ stdioEnvVars.filter((v) => v.isRequired === true && v.default == null).map((v) => v.name),
91
+ );
92
+
93
+ const missingInServerJson = [...manifestUserConfigKeys].filter((k) => !stdioEnvNames.has(k));
94
+ const missingInManifest = [...requiredStdioEnvNames].filter((k) => !manifestEnvKeys.has(k));
95
+
96
+ const errors: string[] = [];
97
+ if (missingInServerJson.length > 0) {
98
+ errors.push(
99
+ `manifest.json references user_config env var(s) not advertised in server.json stdio environmentVariables[]: ${missingInServerJson.join(', ')}`,
100
+ );
101
+ }
102
+ if (missingInManifest.length > 0) {
103
+ errors.push(
104
+ `server.json declares required stdio env var(s) without default missing from manifest.json mcp_config.env: ${missingInManifest.join(', ')}`,
105
+ );
106
+ }
107
+
108
+ if (errors.length === 0) {
109
+ console.log('Packaging alignment OK.');
110
+ process.exit(0);
111
+ }
112
+ for (const err of errors) console.error(` ✗ ${err}`);
113
+ process.exit(1);
114
+ }
115
+
116
+ main();
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * @fileoverview Surfaces the YAML frontmatter of all SKILL.md files in this
4
+ * project's `.claude/skills/` directory (falling back to `skills/`). Mirrors
5
+ * how the Claude Code harness lists available skills, but as plain stdout an
6
+ * agent can read.
7
+ *
8
+ * Sub-agents spawned via the Agent tool do NOT inherit the parent session's
9
+ * skill registry — they see only the parent's skills, not the project-local
10
+ * skills in their working directory. Running this script gives a sub-agent
11
+ * operating in this project a quick index of available local skills. The
12
+ * agent can then read any relevant SKILL.md by the printed path before
13
+ * following its steps.
14
+ *
15
+ * @module scripts/list-skills
16
+ *
17
+ * Usage:
18
+ * bun run scripts/list-skills.ts
19
+ */
20
+
21
+ import { existsSync } from 'node:fs';
22
+ import { readdir, readFile } from 'node:fs/promises';
23
+ import { join, resolve } from 'node:path';
24
+
25
+ const CANDIDATE_DIRS = ['.claude/skills', 'skills'] as const;
26
+
27
+ interface SkillEntry {
28
+ description: string;
29
+ name: string;
30
+ path: string;
31
+ references: string[];
32
+ }
33
+
34
+ /**
35
+ * Naive YAML frontmatter parser. Handles flat `key: value` pairs (with
36
+ * optional surrounding `"` / `'` quotes stripped) and folded (`>`) / literal
37
+ * (`|`) block scalars over indented continuation lines — folded joins lines
38
+ * with spaces, literal preserves newlines. Sufficient for SKILL.md
39
+ * frontmatter — do not extend to general YAML; pull in a real parser if the
40
+ * format outgrows this.
41
+ */
42
+ function parseFrontmatter(content: string): Record<string, string> {
43
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
44
+ const body = match?.[1];
45
+ if (!body) return {};
46
+ const out: Record<string, string> = {};
47
+ const lines = body.split('\n');
48
+ let i = 0;
49
+ while (i < lines.length) {
50
+ const line = lines[i];
51
+ if (line === undefined) break;
52
+ const kv = line.match(/^(\w+):\s*(.*)$/);
53
+ const key = kv?.[1];
54
+ if (!kv || !key) {
55
+ i++;
56
+ continue;
57
+ }
58
+ const val = (kv[2] ?? '').trim();
59
+ const block = val.match(/^([>|])[-+]?$/);
60
+ if (block) {
61
+ const buf: string[] = [];
62
+ i++;
63
+ while (i < lines.length) {
64
+ const next = lines[i];
65
+ if (next === undefined || !/^\s+\S/.test(next)) break;
66
+ buf.push(next.trim());
67
+ i++;
68
+ }
69
+ out[key] = buf.join(block[1] === '|' ? '\n' : ' ').trim();
70
+ continue;
71
+ }
72
+ out[key] = stripQuotes(val);
73
+ i++;
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function stripQuotes(s: string): string {
79
+ if (s.length < 2) return s;
80
+ const first = s[0];
81
+ const last = s[s.length - 1];
82
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
83
+ return s.slice(1, -1);
84
+ }
85
+ return s;
86
+ }
87
+
88
+ async function listReferences(skillDir: string): Promise<string[]> {
89
+ try {
90
+ const entries = await readdir(join(skillDir, 'references'));
91
+ return entries.filter((e) => e.endsWith('.md')).sort();
92
+ } catch (err) {
93
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ function findSkillsDir(): { rel: string; abs: string } | null {
99
+ // Resolve candidates relative to the script's parent dir (project root),
100
+ // not CWD — invocation may be from a sub-agent's worktree or subdirectory.
101
+ const root = resolve(import.meta.dirname, '..');
102
+ for (const dir of CANDIDATE_DIRS) {
103
+ const abs = join(root, dir);
104
+ if (existsSync(abs)) return { rel: dir, abs };
105
+ }
106
+ return null;
107
+ }
108
+
109
+ async function main(): Promise<void> {
110
+ const dir = findSkillsDir();
111
+ const root = resolve(import.meta.dirname, '..');
112
+ if (!dir) {
113
+ console.error(
114
+ `No skills directory found. Expected one of: ${CANDIDATE_DIRS.join(', ')} relative to project root (${root}).`,
115
+ );
116
+ process.exit(1);
117
+ }
118
+
119
+ const entries = await readdir(dir.abs, { withFileTypes: true });
120
+ const skills = (
121
+ await Promise.all(
122
+ entries
123
+ .filter((e) => e.isDirectory())
124
+ .map(async (entry): Promise<SkillEntry | null> => {
125
+ const skillDir = join(dir.abs, entry.name);
126
+ const skillPath = join(skillDir, 'SKILL.md');
127
+ let content: string;
128
+ try {
129
+ content = await readFile(skillPath, 'utf-8');
130
+ } catch (err) {
131
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
132
+ throw err;
133
+ }
134
+ const fm = parseFrontmatter(content);
135
+ return {
136
+ name: fm.name ?? entry.name,
137
+ description: fm.description ?? '',
138
+ path: skillPath,
139
+ references: await listReferences(skillDir),
140
+ };
141
+ }),
142
+ )
143
+ ).filter((s): s is SkillEntry => s !== null);
144
+
145
+ skills.sort((a, b) => a.name.localeCompare(b.name));
146
+
147
+ console.log(`# Skills available in ${dir.rel}/`);
148
+ console.log(`# Project root: ${root}`);
149
+ console.log(`#`);
150
+ console.log(`# Sub-agents: this list mimics the parent harness's skill registry.`);
151
+ console.log(
152
+ `# Read the full SKILL.md at the listed path before following a skill's procedure.\n`,
153
+ );
154
+
155
+ for (const s of skills) {
156
+ console.log(`- ${s.name} (${s.path})`);
157
+ if (s.description) console.log(` ${s.description}`);
158
+ if (s.references.length > 0) {
159
+ console.log(` references: ${s.references.join(', ')}`);
160
+ }
161
+ console.log();
162
+ }
163
+
164
+ console.log(`Total: ${skills.length} skills`);
165
+ }
166
+
167
+ main().catch((err) => {
168
+ console.error(err);
169
+ process.exit(1);
170
+ });
@@ -4,7 +4,7 @@ description: >
4
4
  Cloudflare Workers deployment using `createWorkerHandler` from `@cyanheads/mcp-ts-core/worker`. Covers the full handler signature, binding types, CloudflareBindings extensibility, runtime compatibility guards, and wrangler.toml requirements.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.3"
7
+ version: "1.4"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -49,6 +49,7 @@ Fresh scaffolds register definitions directly in the entry point as shown above.
49
49
  | `resources` | `AnyResourceDefinition[]` | Resource definitions to register |
50
50
  | `prompts` | `PromptDefinition[]` | Prompt definitions to register |
51
51
  | `extensions` | `Record<string, object>` | SEP-2133 extensions to advertise in server capabilities |
52
+ | `instructions` | `string \| (env: CloudflareBindings) => string` | Server-level orientation forwarded to the model on every `initialize`. Resolver form runs inside `initializeApp(env)` so env-derived text is available (see Workers-specific warnings). Empty string treated as unset. |
52
53
  | `setup` | `(core: CoreServices) => void \| Promise<void>` | Runs after core services are ready, during the first request (lazy init inside the fetch handler) |
53
54
  | `extraEnvBindings` | `[bindingKey: string, processEnvKey: string][]` | Maps CF string bindings to `process.env` keys |
54
55
  | `extraObjectBindings` | `[bindingKey: string, globalKey: string][]` | Maps CF object bindings (KV, R2, D1, AI) to `globalThis` keys |
@@ -156,6 +157,19 @@ bucket_name = "..."
156
157
 
157
158
  ## Workers-specific warnings
158
159
 
160
+ **`instructions` resolver runs after env injection.** When `instructions` is a function, it runs inside `initializeApp(env)` — after `injectEnvVars()` — so env-derived text reaches the model without fighting the Workers module-load lifecycle:
161
+
162
+ ```ts
163
+ export default createWorkerHandler({
164
+ tools: [echoTool],
165
+ instructions: (env) =>
166
+ `Region: ${env.ENVIRONMENT ?? 'production'}.` +
167
+ (env.MAINTENANCE_MODE ? ' Read-only mode — writes disabled.' : ''),
168
+ });
169
+ ```
170
+
171
+ Plain strings work the same as on `createApp`. Type extends `Omit<CreateAppOptions, 'instructions'>`, so this is the only option whose shape differs between Node and Worker entry points.
172
+
159
173
  **Lazy env parsing is mandatory.** Cloudflare injects env bindings at request time via `injectEnvVars()`, after all static module imports complete. Never parse `process.env` at module top-level in Workers:
160
174
 
161
175
  ```ts