@cyanheads/mcp-ts-core 0.12.9 → 0.13.1

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 (126) hide show
  1. package/AGENTS.md +22 -11
  2. package/CLAUDE.md +22 -11
  3. package/README.md +1 -1
  4. package/biome.json +1 -1
  5. package/changelog/0.13.x/0.13.0.md +48 -0
  6. package/changelog/0.13.x/0.13.1.md +56 -0
  7. package/changelog/template.md +7 -24
  8. package/{tsconfig.base.json → config/tsconfig.base.json} +2 -2
  9. package/dist/cli/init.js +2 -2
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/config/envValue.d.ts +18 -0
  12. package/dist/config/envValue.d.ts.map +1 -0
  13. package/dist/config/envValue.js +35 -0
  14. package/dist/config/envValue.js.map +1 -0
  15. package/dist/config/index.d.ts +8 -0
  16. package/dist/config/index.d.ts.map +1 -1
  17. package/dist/config/index.js +13 -7
  18. package/dist/config/index.js.map +1 -1
  19. package/dist/config/parseEnvConfig.d.ts +7 -0
  20. package/dist/config/parseEnvConfig.d.ts.map +1 -1
  21. package/dist/config/parseEnvConfig.js +9 -1
  22. package/dist/config/parseEnvConfig.js.map +1 -1
  23. package/dist/core/app.d.ts +82 -2
  24. package/dist/core/app.d.ts.map +1 -1
  25. package/dist/core/app.js +129 -6
  26. package/dist/core/app.js.map +1 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/worker.d.ts +6 -1
  31. package/dist/core/worker.d.ts.map +1 -1
  32. package/dist/core/worker.js.map +1 -1
  33. package/dist/linter/rules/resource-rules.js +9 -2
  34. package/dist/linter/rules/resource-rules.js.map +1 -1
  35. package/dist/linter/rules/tool-rules.js +4 -1
  36. package/dist/linter/rules/tool-rules.js.map +1 -1
  37. package/dist/linter/validate.js +2 -2
  38. package/dist/linter/validate.js.map +1 -1
  39. package/dist/mcp-server/types.d.ts +10 -3
  40. package/dist/mcp-server/types.d.ts.map +1 -1
  41. package/dist/mcp-server/types.js +4 -3
  42. package/dist/mcp-server/types.js.map +1 -1
  43. package/dist/services/canvas/core/sqlGate.d.ts.map +1 -1
  44. package/dist/services/canvas/core/sqlGate.js +27 -2
  45. package/dist/services/canvas/core/sqlGate.js.map +1 -1
  46. package/dist/utils/pagination/pagination.d.ts.map +1 -1
  47. package/dist/utils/pagination/pagination.js +4 -1
  48. package/dist/utils/pagination/pagination.js.map +1 -1
  49. package/dist/utils/parsing/frontmatterParser.d.ts +8 -7
  50. package/dist/utils/parsing/frontmatterParser.d.ts.map +1 -1
  51. package/dist/utils/parsing/frontmatterParser.js +91 -16
  52. package/dist/utils/parsing/frontmatterParser.js.map +1 -1
  53. package/framework-skills/README.md +40 -0
  54. package/{skills → framework-skills}/add-app-tool/SKILL.md +2 -2
  55. package/{skills → framework-skills}/add-resource/SKILL.md +2 -2
  56. package/{skills → framework-skills}/add-service/SKILL.md +2 -2
  57. package/{skills → framework-skills}/add-test/SKILL.md +2 -2
  58. package/{skills → framework-skills}/add-tool/SKILL.md +5 -5
  59. package/{skills → framework-skills}/api-config/SKILL.md +21 -3
  60. package/{skills → framework-skills}/api-context/SKILL.md +5 -3
  61. package/{skills → framework-skills}/api-linter/SKILL.md +4 -4
  62. package/{skills → framework-skills}/api-telemetry/SKILL.md +13 -10
  63. package/{skills → framework-skills}/code-simplifier/SKILL.md +12 -6
  64. package/{skills → framework-skills}/design-mcp-server/SKILL.md +2 -2
  65. package/{skills → framework-skills}/maintenance/SKILL.md +30 -21
  66. package/{skills → framework-skills}/orchestrations/SKILL.md +2 -2
  67. package/{skills → framework-skills}/orchestrations/workflows/field-test-fix.md +8 -8
  68. package/{skills → framework-skills}/orchestrations/workflows/fix-wrapup-release.md +5 -5
  69. package/{skills → framework-skills}/orchestrations/workflows/greenfield-build.md +11 -11
  70. package/{skills → framework-skills}/orchestrations/workflows/maintenance-release.md +12 -12
  71. package/{skills → framework-skills}/polish-docs-meta/SKILL.md +18 -10
  72. package/{skills → framework-skills}/polish-docs-meta/references/agent-protocol.md +1 -1
  73. package/{skills → framework-skills}/polish-docs-meta/references/readme.md +93 -73
  74. package/{skills → framework-skills}/release-and-publish/SKILL.md +12 -3
  75. package/{skills → framework-skills}/release-pr-review/SKILL.md +2 -2
  76. package/{skills → framework-skills}/report-issue-framework/SKILL.md +26 -25
  77. package/{skills → framework-skills}/report-issue-local/SKILL.md +28 -24
  78. package/{skills → framework-skills}/setup/SKILL.md +10 -8
  79. package/package.json +13 -13
  80. package/scripts/build.ts +2 -2
  81. package/scripts/check-framework-antipatterns.ts +1 -1
  82. package/scripts/check-skill-versions.ts +16 -9
  83. package/scripts/check-skills-sync.ts +64 -13
  84. package/scripts/clean-mcpb.ts +3 -3
  85. package/scripts/devcheck.ts +18 -15
  86. package/scripts/lint-packaging.ts +158 -24
  87. package/scripts/list-skills.ts +2 -2
  88. package/templates/.claude-plugin/plugin.json +5 -1
  89. package/templates/.env.example +5 -2
  90. package/templates/.github/CONTRIBUTING.md +4 -5
  91. package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +5 -4
  92. package/templates/.github/ISSUE_TEMPLATE/config.yml +6 -1
  93. package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -2
  94. package/templates/AGENTS.md +31 -14
  95. package/templates/CLAUDE.md +31 -14
  96. package/templates/_.mcpbignore +1 -1
  97. package/templates/changelog/template.md +7 -24
  98. package/templates/package.json +3 -2
  99. package/templates/src/index.ts +10 -0
  100. package/templates/src/mcp-server/resources/definitions/echo-app-ui.app-resource.ts +1 -1
  101. package/skills/README.md +0 -38
  102. /package/{skills → framework-skills}/add-export/SKILL.md +0 -0
  103. /package/{skills → framework-skills}/add-prompt/SKILL.md +0 -0
  104. /package/{skills → framework-skills}/add-provider/SKILL.md +0 -0
  105. /package/{skills → framework-skills}/api-auth/SKILL.md +0 -0
  106. /package/{skills → framework-skills}/api-canvas/SKILL.md +0 -0
  107. /package/{skills → framework-skills}/api-errors/SKILL.md +0 -0
  108. /package/{skills → framework-skills}/api-mirror/SKILL.md +0 -0
  109. /package/{skills → framework-skills}/api-services/SKILL.md +0 -0
  110. /package/{skills → framework-skills}/api-services/references/graph.md +0 -0
  111. /package/{skills → framework-skills}/api-services/references/llm.md +0 -0
  112. /package/{skills → framework-skills}/api-services/references/speech.md +0 -0
  113. /package/{skills → framework-skills}/api-testing/SKILL.md +0 -0
  114. /package/{skills → framework-skills}/api-utils/SKILL.md +0 -0
  115. /package/{skills → framework-skills}/api-utils/references/formatting.md +0 -0
  116. /package/{skills → framework-skills}/api-utils/references/parsing.md +0 -0
  117. /package/{skills → framework-skills}/api-utils/references/security.md +0 -0
  118. /package/{skills → framework-skills}/api-workers/SKILL.md +0 -0
  119. /package/{skills → framework-skills}/field-test/SKILL.md +0 -0
  120. /package/{skills → framework-skills}/git-wrapup/SKILL.md +0 -0
  121. /package/{skills → framework-skills}/polish-docs-meta/references/package-meta.md +0 -0
  122. /package/{skills → framework-skills}/polish-docs-meta/references/server-json.md +0 -0
  123. /package/{skills → framework-skills}/security-pass/SKILL.md +0 -0
  124. /package/{skills → framework-skills}/techniques/SKILL.md +0 -0
  125. /package/{skills → framework-skills}/techniques/references/outline-on-overflow.md +0 -0
  126. /package/{skills → framework-skills}/tool-defs-analysis/SKILL.md +0 -0
@@ -19,13 +19,13 @@
19
19
  * 5. Bundle-content guard: known root dev directories must not appear at
20
20
  * bundle root after `.mcpbignore` evaluation (dev dir not excluded).
21
21
  * 6. Bundle-content guard: `.mcpbignore` must not use unanchored patterns
22
- * for root dev dirs — an unanchored `skills/` also strips
23
- * `node_modules/x/skills/` (runtime path bypass, issues #172/#207).
22
+ * for root dev dirs — an unanchored `framework-skills/` also strips
23
+ * `node_modules/x/framework-skills/` (runtime path bypass, issues #172/#207).
24
24
  * 7. Bundle-content guard: `.mcpbignore` patterns must not strip critical
25
25
  * runtime package paths (e.g. `node_modules/@opentelemetry/api/build/src/`).
26
26
  * 8. Post-bundle content: a built `.mcpb` under `dist/` must contain zero
27
- * `node_modules/**` agent-doc entries (dependency-shipped `skills/`,
28
- * `.claude/`, `.agents/`, `SKILL.md`) — unreachable by root-anchored
27
+ * `node_modules/**` agent-doc entries (dependency-shipped `framework-skills/`,
28
+ * `skills/`, `.claude/`, `.agents/`, `SKILL.md`) — unreachable by root-anchored
29
29
  * `.mcpbignore` patterns; `scripts/clean-mcpb.ts` strips them at bundle
30
30
  * time (issue #230).
31
31
  * 9. Identity: `name`/`title` literals in `createApp()` /
@@ -41,9 +41,19 @@
41
41
  * is a guaranteed install 404. Each present plugin manifest's `version`
42
42
  * must equal `package.json`'s, so a release cannot ship stale plugin
43
43
  * metadata (issue #393); `.codex-plugin/mcp.json` is connection config and
44
- * carries no version. Gated by `devcheck.config.json`
45
- * `packaging.pluginManifests` (default on); each manifest is skipped
46
- * cleanly when absent (issue #240).
44
+ * carries no version. No server `env` value may be the empty string: the
45
+ * client sets it on the child process, so the placeholder replaces a key
46
+ * the user exported and the framework then reads it as unset. Claude Code
47
+ * takes user values through `userConfig` + `${user_config.<key>}` (every
48
+ * reference must be declared); Codex forwards host variables named in
49
+ * `env_vars`. Gated by `devcheck.config.json` `packaging.pluginManifests`
50
+ * (default on); each manifest is skipped cleanly when absent (issue #240).
51
+ * 11. MCPB `user_config` wiring: every declared option is referenced from
52
+ * `mcp_config` as `${user_config.<key>}`, every reference is declared,
53
+ * `mcp_config` carries no other `${…}` placeholder besides the host's
54
+ * path variables (the host delivers anything else as the literal string),
55
+ * and an optional string option has `"default": ""` so a blank answer
56
+ * arrives as empty rather than as the unsubstituted placeholder.
47
57
  *
48
58
  * Every check skips cleanly when its input is absent — consumers who deleted
49
59
  * `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
@@ -80,7 +90,7 @@ interface ManifestUserConfigEntry {
80
90
  interface Manifest {
81
91
  display_name?: unknown;
82
92
  name?: string;
83
- server?: { mcp_config?: { env?: Record<string, string> } };
93
+ server?: { mcp_config?: { args?: unknown[]; env?: Record<string, string> } };
84
94
  user_config?: Record<string, ManifestUserConfigEntry>;
85
95
  }
86
96
 
@@ -89,15 +99,15 @@ const USER_CONFIG_REF = /^\$\{user_config\.([\w-]+)\}$/;
89
99
  /**
90
100
  * Root dev directories the scaffold template excludes from the bundle, and
91
101
  * whose `.mcpbignore` patterns must be anchored with `/` to avoid also
92
- * stripping nested runtime paths like `node_modules/x/skills/`. Keep in step
102
+ * stripping nested runtime paths like `node_modules/x/framework-skills/`. Keep in step
93
103
  * with the directory entries in `templates/_.mcpbignore`.
94
104
  */
95
- export const KNOWN_DEV_DIRS = ['skills/', '.agents/', '.claude/'];
105
+ export const KNOWN_DEV_DIRS = ['framework-skills/', '.agents/', '.claude/'];
96
106
 
97
107
  /**
98
108
  * Critical runtime paths that must NOT be stripped by any `.mcpbignore` pattern.
99
- * These are sampled representative paths — enough to catch a bare `skills/`
100
- * pattern accidentally stripping `node_modules/…/skills/`.
109
+ * These are sampled representative paths — enough to catch a bare `framework-skills/`
110
+ * pattern accidentally stripping `node_modules/…/framework-skills/`.
101
111
  */
102
112
  export const CRITICAL_RUNTIME_PATHS = [
103
113
  'node_modules/@opentelemetry/api/build/src/',
@@ -108,11 +118,13 @@ export const CRITICAL_RUNTIME_PATHS = [
108
118
 
109
119
  /**
110
120
  * Agent-doc entries under `node_modules/` that must not ship in a bundle.
121
+ * `framework-skills/` is this framework's tree; `skills/` covers any other
122
+ * dependency that vendors agent skills.
111
123
  * KEEP IN SYNC with `AGENT_DOC_ENTRY` in `scripts/clean-mcpb.ts` (the strip
112
124
  * step this check verifies) — a unit test asserts the two are identical.
113
125
  */
114
126
  export const AGENT_DOC_ENTRY =
115
- /^node_modules\/.*(?:\/skills\/|\/\.claude\/|\/\.agents\/|\/SKILL\.md$)/;
127
+ /^node_modules\/.*(?:\/framework-skills\/|\/skills\/|\/\.claude\/|\/\.agents\/|\/SKILL\.md$)/;
116
128
 
117
129
  /**
118
130
  * Platform-specific native binding packages that must not ship in a bundle.
@@ -251,7 +263,7 @@ export function checkBundleEntries(entries: string[], bundleLabel: string): stri
251
263
  if (agentDocs.length > 0) {
252
264
  errors.push(
253
265
  `${bundleLabel} contains ${agentDocs.length} node_modules agent-doc entries ` +
254
- `(dependency-shipped skills/, .claude/, .agents/, SKILL.md) — re-run the \`bundle\` ` +
266
+ `(dependency-shipped framework-skills/, skills/, .claude/, .agents/, SKILL.md) — re-run the \`bundle\` ` +
255
267
  `script (scripts/clean-mcpb.ts strips them):${sampleOf(agentDocs)}`,
256
268
  );
257
269
  }
@@ -409,6 +421,81 @@ export function checkManifestIdentity(manifest: Manifest, unscopedName: string):
409
421
  return [];
410
422
  }
411
423
 
424
+ /** Placeholders the MCPB host substitutes in `mcp_config` besides `${user_config.<key>}`. */
425
+ const MCPB_HOST_VARS = new Set([
426
+ '__dirname',
427
+ 'HOME',
428
+ 'DESKTOP',
429
+ 'DOCUMENTS',
430
+ 'DOWNLOADS',
431
+ 'pathSeparator',
432
+ '/',
433
+ ]);
434
+
435
+ /**
436
+ * Check 11: MCPB `user_config` wiring. A value the host collects from the
437
+ * user reaches the server only through a `${user_config.<key>}` reference in
438
+ * `mcp_config`; the host substitutes nothing else except its own path
439
+ * placeholders, so `${API_KEY}` is delivered as that literal string. Every
440
+ * declared option must therefore be referenced, every reference must be
441
+ * declared, and an optional string option needs `"default": ""` so a blank
442
+ * answer arrives as empty rather than as the unsubstituted placeholder.
443
+ */
444
+ export function checkManifestUserConfigWiring(manifest: Manifest): string[] {
445
+ const errors: string[] = [];
446
+ const userConfig = manifest.user_config ?? {};
447
+ const env = manifest.server?.mcp_config?.env ?? {};
448
+ const args = manifest.server?.mcp_config?.args ?? [];
449
+ const referenced = new Set<string>();
450
+
451
+ const scan = (value: unknown, where: string): void => {
452
+ if (typeof value !== 'string') return;
453
+ for (const match of value.matchAll(/\$\{([^}]+)\}/g)) {
454
+ const token = match[1] ?? '';
455
+ if (token.startsWith('user_config.')) {
456
+ const key = token.slice('user_config.'.length);
457
+ referenced.add(key);
458
+ if (!(key in userConfig)) {
459
+ errors.push(
460
+ `manifest.json ${where} references "\${user_config.${key}}" but user_config["${key}"] is not declared`,
461
+ );
462
+ }
463
+ } else if (!MCPB_HOST_VARS.has(token)) {
464
+ errors.push(
465
+ `manifest.json ${where} references "\${${token}}" — MCPB substitutes only \${user_config.<key>} and its ` +
466
+ `own path placeholders, so the server receives that literal string; declare the option under user_config ` +
467
+ `and reference "\${user_config.${token}}"`,
468
+ );
469
+ }
470
+ }
471
+ };
472
+ for (const [key, value] of Object.entries(env)) scan(value, `mcp_config.env.${key}`);
473
+ for (const [i, value] of args.entries()) scan(value, `mcp_config.args[${i}]`);
474
+
475
+ for (const [key, entry] of Object.entries(userConfig)) {
476
+ if (!referenced.has(key)) {
477
+ errors.push(
478
+ `manifest.json user_config["${key}"] is never referenced from mcp_config — the host collects the value ` +
479
+ `and then drops it; add "${key}": "\${user_config.${key}}" to mcp_config.env`,
480
+ );
481
+ }
482
+ if (
483
+ typeof entry === 'object' &&
484
+ entry !== null &&
485
+ entry.type === 'string' &&
486
+ entry.required !== true &&
487
+ !('default' in entry)
488
+ ) {
489
+ errors.push(
490
+ `manifest.json user_config["${key}"] is an optional string with no "default" — a blank answer reaches ` +
491
+ `the server as the literal "\${user_config.${key}}"; add "default": ""`,
492
+ );
493
+ }
494
+ }
495
+
496
+ return errors;
497
+ }
498
+
412
499
  /** Parsed plugin marketplace manifests; an absent manifest is `undefined`. */
413
500
  export interface PluginManifestInputs {
414
501
  claudePlugin?: unknown;
@@ -424,6 +511,11 @@ function installArg(entry: Record<string, unknown>): unknown {
424
511
  return Array.isArray(entry.args) ? entry.args[1] : undefined;
425
512
  }
426
513
 
514
+ /** A server entry's `env` object, or an empty one when absent or malformed. */
515
+ function serverEnv(entry: Record<string, unknown>): Record<string, unknown> {
516
+ return isRecord(entry.env) ? entry.env : {};
517
+ }
518
+
427
519
  /**
428
520
  * Check 10: plugin marketplace manifests. Display fields (`name`, server key,
429
521
  * `interface.displayName`) must equal the unscoped machine name; the install
@@ -434,6 +526,15 @@ function installArg(entry: Record<string, unknown>): unknown {
434
526
  * and non-plugin consumers are unaffected. The caller gates the whole check on
435
527
  * `packaging.pluginManifests`.
436
528
  *
529
+ * A server `env` value of `""` is rejected in both connection configs. The
530
+ * client sets the entry on the child process, so the placeholder replaces a
531
+ * key the user exported and the framework's empty-string-as-unset parsing then
532
+ * drops it — the user's real value never reaches the server. Claude Code
533
+ * collects user values through `userConfig` and substitutes
534
+ * `${user_config.<key>}` in `env`; every such reference must name a declared
535
+ * option. Codex launches stdio servers with a whitelisted environment, so a
536
+ * host variable reaches the server only when `env_vars` names it.
537
+ *
437
538
  * `.codex-plugin/mcp.json` is connection configuration and carries no version.
438
539
  */
439
540
  export function checkPluginManifests(
@@ -478,11 +579,31 @@ export function checkPluginManifests(
478
579
  );
479
580
  }
480
581
  const entry = servers[unscopedName];
481
- if (isRecord(entry) && installArg(entry) !== fullName) {
482
- errors.push(
483
- `${f} mcpServers["${unscopedName}"] install arg is "${String(installArg(entry))}" — ` +
484
- `must be the full package name "${fullName}" (the npx -y target; an unscoped arg for a scoped package 404s)`,
485
- );
582
+ if (isRecord(entry)) {
583
+ if (installArg(entry) !== fullName) {
584
+ errors.push(
585
+ `${f} mcpServers["${unscopedName}"] install arg is "${String(installArg(entry))}" — ` +
586
+ `must be the full package name "${fullName}" (the npx -y target; an unscoped arg for a scoped package 404s)`,
587
+ );
588
+ }
589
+ const userConfig = isRecord(claude.userConfig) ? claude.userConfig : {};
590
+ for (const [key, value] of Object.entries(serverEnv(entry))) {
591
+ if (value === '') {
592
+ errors.push(
593
+ `${f} mcpServers["${unscopedName}"].env.${key} is "" — an empty placeholder replaces the ` +
594
+ `user's exported ${key} and is read as unset; declare the option under "userConfig" ` +
595
+ `and set the value to "\${user_config.<option>}"`,
596
+ );
597
+ } else if (typeof value === 'string') {
598
+ const ref = USER_CONFIG_REF.exec(value)?.[1];
599
+ if (ref !== undefined && !isRecord(userConfig[ref])) {
600
+ errors.push(
601
+ `${f} mcpServers["${unscopedName}"].env.${key} references "\${user_config.${ref}}" but ` +
602
+ `"userConfig.${ref}" is not declared — Claude Code prompts only for declared options`,
603
+ );
604
+ }
605
+ }
606
+ }
486
607
  }
487
608
  }
488
609
  }
@@ -530,11 +651,22 @@ export function checkPluginManifests(
530
651
  );
531
652
  }
532
653
  const entry = codexMcp[unscopedName];
533
- if (isRecord(entry) && installArg(entry) !== fullName) {
534
- errors.push(
535
- `${f} "${unscopedName}" install arg is "${String(installArg(entry))}" — ` +
536
- `must be the full package name "${fullName}" (the npx -y target)`,
537
- );
654
+ if (isRecord(entry)) {
655
+ if (installArg(entry) !== fullName) {
656
+ errors.push(
657
+ `${f} "${unscopedName}" install arg is "${String(installArg(entry))}" — ` +
658
+ `must be the full package name "${fullName}" (the npx -y target)`,
659
+ );
660
+ }
661
+ for (const [key, value] of Object.entries(serverEnv(entry))) {
662
+ if (value === '') {
663
+ errors.push(
664
+ `${f} "${unscopedName}".env.${key} is "" — Codex starts stdio servers with a whitelisted ` +
665
+ `environment, so an empty placeholder adds nothing; remove it and list "${key}" in ` +
666
+ `"env_vars" to forward the user's value`,
667
+ );
668
+ }
669
+ }
538
670
  }
539
671
  }
540
672
 
@@ -586,6 +718,8 @@ async function main(): Promise<void> {
586
718
  }
587
719
  }
588
720
 
721
+ errors.push(...checkManifestUserConfigWiring(manifest));
722
+
589
723
  const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
590
724
  if (serverJson) {
591
725
  const manifestEnv = manifest.server?.mcp_config?.env ?? {};
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * @fileoverview Surfaces the YAML frontmatter of all SKILL.md files in this
4
- * project's `.claude/skills/` directory (falling back to `skills/`). Mirrors
4
+ * project's `.claude/skills/` directory (falling back to `framework-skills/`). Mirrors
5
5
  * how the Claude Code harness lists available skills, but as plain stdout an
6
6
  * agent can read.
7
7
  *
@@ -22,7 +22,7 @@ import { existsSync } from 'node:fs';
22
22
  import { readdir, readFile } from 'node:fs/promises';
23
23
  import { join, resolve } from 'node:path';
24
24
 
25
- const CANDIDATE_DIRS = ['.claude/skills', 'skills'] as const;
25
+ const CANDIDATE_DIRS = ['.claude/skills', 'framework-skills'] as const;
26
26
 
27
27
  interface SkillEntry {
28
28
  description: string;
@@ -1,13 +1,17 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
2
3
  "name": "{{PACKAGE_NAME}}",
3
4
  "version": "0.1.0",
4
5
  "description": "",
5
6
  "author": {
6
- "name": ""
7
+ "name": "",
8
+ "email": "",
9
+ "url": ""
7
10
  },
8
11
  "homepage": "",
9
12
  "repository": "",
10
13
  "license": "Apache-2.0",
14
+ "keywords": ["mcp", "mcp-server", "model-context-protocol"],
11
15
  "mcpServers": {
12
16
  "{{PACKAGE_NAME}}": {
13
17
  "command": "npx",
@@ -1,7 +1,7 @@
1
1
  # ── Transport ──────────────────────────────────────────────────────────
2
2
  # MCP_TRANSPORT_TYPE=stdio # stdio | http (default: stdio)
3
3
  # MCP_HTTP_PORT=3010 # HTTP port (default: 3010)
4
- # MCP_HTTP_HOST=localhost # HTTP host (default: localhost)
4
+ # MCP_HTTP_HOST=127.0.0.1 # HTTP host (default: 127.0.0.1)
5
5
  # MCP_HTTP_ENDPOINT_PATH=/mcp # HTTP endpoint path (default: /mcp)
6
6
  # MCP_HTTP_MAX_BODY_BYTES=1048576 # Max request body bytes; 413 over limit, 0 disables (default: 1048576)
7
7
  # MCP_PUBLIC_URL= # Public origin behind a TLS-terminating proxy (e.g. https://mcp.example.com)
@@ -14,7 +14,10 @@
14
14
  # STORAGE_PROVIDER_TYPE=in-memory # in-memory | filesystem | supabase | cloudflare-r2 | cloudflare-kv | cloudflare-d1
15
15
 
16
16
  # ── Session ──────────────────────────────────────────────────────────
17
- # MCP_SESSION_MODE=stateful # stateful | stateless (default: stateful)
17
+ MCP_SESSION_MODE=stateless # stateful | stateless | auto. Set here, not left to the schema default
18
+ # (auto, which resolves to stateful). Stateless fits a data API:
19
+ # no session store, horizontally scalable. Use stateful when a tool
20
+ # asks the caller for input mid-handler (ctx.requestInput).
18
21
  # MCP_HTTP_RESUMABILITY=false # SSE replay on a dropped stateful stream. Default: true.
19
22
  # Kill switch only — no effect on stateless serving.
20
23
  # MCP_HTTP_RESUMABILITY_MAX_EVENTS=512 # Retained per session, oldest evicted first
@@ -2,13 +2,12 @@
2
2
 
3
3
  Thanks for using `{{PACKAGE_NAME}}`. Bugs, feature requests, and documentation gaps all belong in an issue — that's where they get read and picked up.
4
4
 
5
- Open one from the **Issues** tab and pick the **Bug Report** or **Feature Request** form. Both are structured, and filling in the fields is what makes an issue actionable.
5
+ Open one from the **Issues** tab and pick the **Bug Report** or **Feature Request** form. Both are structured, and filling in the fields is what makes an issue actionable. Anything that fits neither can be a plain issue — a half-formed idea in your own words is fine.
6
6
 
7
7
  <!-- Optional: swap the line above for direct links once you know your repo URL —
8
8
  https://github.com/OWNER/REPO/issues/new?template=bug_report.yml -->
9
9
 
10
- <!-- If you accept pull requests, say so here — e.g. "PRs welcome; open an issue
11
- first for anything larger than a typo." Silence reads as "issues only". -->
10
+ <!-- This project takes contributions as issues. Do not add a "pull requests are welcome" line. -->
12
11
 
13
12
  ## Server bug or framework bug?
14
13
 
@@ -40,8 +39,8 @@ Do the triage first — an unverified report costs more to read than it saves to
40
39
 
41
40
  Two workflows ship with this project:
42
41
 
43
- - [`skills/report-issue-local/SKILL.md`](../skills/report-issue-local/SKILL.md) — filing against this repo.
44
- - [`skills/report-issue-framework/SKILL.md`](../skills/report-issue-framework/SKILL.md) — filing against `mcp-ts-core` when you've isolated the bug to the framework.
42
+ - [`framework-skills/report-issue-local/SKILL.md`](../framework-skills/report-issue-local/SKILL.md) — filing against this repo.
43
+ - [`framework-skills/report-issue-framework/SKILL.md`](../framework-skills/report-issue-framework/SKILL.md) — filing against `mcp-ts-core` when you've isolated the bug to the framework.
45
44
 
46
45
  Read the relevant one before filing on a user's behalf.
47
46
 
@@ -2,11 +2,14 @@ name: Bug Report
2
2
  description: Report a bug in {{PACKAGE_NAME}}
3
3
  labels: ["bug"]
4
4
  # assignees: ["your-github-username"] # uncomment to auto-assign new issues
5
+ # The secondary labels listed below must exist in the repo — create each once: gh label create <name>
5
6
  body:
6
7
  - type: markdown
7
8
  attributes:
8
9
  value: |
9
- **Do not include secrets, credentials, API keys, or tokens.** Redact sensitive values from env vars, headers, and logs before submitting. GitHub issues are public.
10
+ **Server bug or framework bug?** File here if a tool returns wrong data, an upstream API call fails, a schema doesn't match reality, or a description misleads the model. If a builder rejects valid input, `createApp()` fails on a valid config, or transport or auth misbehaves regardless of which tool you call, file against [mcp-ts-core](https://github.com/cyanheads/mcp-ts-core/issues) instead. Not sure? File here and it'll get routed.
11
+
12
+ **Do not include secrets, credentials, API keys, tokens, PII, or other sensitive data.** Redact env vars, auth headers, user-identifying information, and internal URLs from all code snippets, logs, and stack traces before submitting. GitHub issues are public and permanent.
10
13
 
11
14
  **Secondary labels.** `bug` is applied automatically — also add one or more of the following in the sidebar if they apply:
12
15
  - `regression` — worked before, broken after a change
@@ -15,8 +18,6 @@ body:
15
18
  - `breaking-change` — fix will break public API
16
19
  - `surplus-token-idea` — worth exploring when token budget allows
17
20
 
18
- If a label doesn't exist in this repo yet, create it once: `gh label create <name>`.
19
-
20
21
  - type: input
21
22
  id: version
22
23
  attributes:
@@ -48,7 +49,7 @@ body:
48
49
  id: runtime-version
49
50
  attributes:
50
51
  label: Runtime version
51
- placeholder: "Bun 1.2.x / Node 22.x"
52
+ placeholder: "Bun 1.4.x / Node 24.x"
52
53
  validations:
53
54
  required: true
54
55
 
@@ -1 +1,6 @@
1
- blank_issues_enabled: false
1
+ blank_issues_enabled: true
2
+ # Optional: route security reports to the private advisory form once you know your repo URL —
3
+ # contact_links:
4
+ # - name: Report a security vulnerability
5
+ # url: https://github.com/OWNER/REPO/security/advisories/new
6
+ # about: Private report — please don't file vulnerabilities as public issues
@@ -2,6 +2,7 @@ name: Feature Request
2
2
  description: Suggest a new feature or improvement for {{PACKAGE_NAME}}
3
3
  labels: ["enhancement"]
4
4
  # assignees: ["your-github-username"] # uncomment to auto-assign new issues
5
+ # The secondary labels listed below must exist in the repo — create each once: gh label create <name>
5
6
  body:
6
7
  - type: markdown
7
8
  attributes:
@@ -12,8 +13,6 @@ body:
12
13
  - `breaking-change` — will break public API; requires a major bump
13
14
  - `surplus-token-idea` — worth exploring when token budget allows
14
15
 
15
- If a label doesn't exist in this repo yet, create it once: `gh label create <name>`.
16
-
17
16
  - type: textarea
18
17
  id: use-case
19
18
  attributes:
@@ -17,10 +17,10 @@ This project was just scaffolded with `bunx @cyanheads/mcp-ts-core init`. You're
17
17
 
18
18
  > **Remove this section** from CLAUDE.md / AGENTS.md after completing these steps. The skills and conventions below remain — this block is one-time onboarding only.
19
19
 
20
- 1. **Get your bearings.** Take stock of the project tree, the skills in `skills/`, and the tools/MCP servers available. Light tool use is fine for context-building — you're mapping the territory, not committing yet.
20
+ 1. **Get your bearings.** Take stock of the project tree, the skills in `framework-skills/`, and the tools/MCP servers available. Light tool use is fine for context-building — you're mapping the territory, not committing yet.
21
21
  2. **Read the framework docs** — `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` (builders, Context, errors, exports, conventions)
22
- 3. **Run the `setup` skill** — read `skills/setup/SKILL.md` and follow its checklist (project orientation, agent protocol file selection, echo definition cleanup, skill sync)
23
- 4. **Design the server** — read `skills/design-mcp-server/SKILL.md` and work through it with the user to map the domain into tools, resources, and services before scaffolding
22
+ 3. **Run the `setup` skill** — read `framework-skills/setup/SKILL.md` and follow its checklist (project orientation, agent protocol file selection, echo definition cleanup, skill sync)
23
+ 4. **Design the server** — read `framework-skills/design-mcp-server/SKILL.md` and work through it with the user to map the domain into tools, resources, and services before scaffolding
24
24
 
25
25
  ---
26
26
 
@@ -174,6 +174,22 @@ await createApp({
174
174
 
175
175
  `instructions` is optional server-level orientation, sent on every `initialize` as session-level context. Use it for deployment guidance (connection aliases, regional notes, scope hints) instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.
176
176
 
177
+ ### Session posture and shutdown
178
+
179
+ Two more `createApp()` options shape how the server runs rather than how it presents itself:
180
+
181
+ ```ts
182
+ await createApp({
183
+ sessionMode: 'stateless', // or { default: 'stateful', require: 'stateful' }
184
+ setup(core) { startMyWatcher(core.config); },
185
+ async teardown() { await stopMyWatcher(); },
186
+ });
187
+ ```
188
+
189
+ `sessionMode` declares the HTTP session posture in `src/` instead of leaving it to a deployment's `MCP_SESSION_MODE`, which still wins whenever it carries a meaningful value (an empty string and an unsubstituted `${…}` placeholder read as unset and fall through to the option). Add `require: 'stateful'` when a tool asks the caller for input mid-handler via `ctx.requestInput`: startup then fails with a `ConfigurationError` rather than serving a mode in which a 2025-era client can never answer the prompt. Stdio is never refused.
190
+
191
+ `teardown(core)` is the `setup()` counterpart — release a watcher, socket, or non-`unref()`'d timer there. It runs after the transport stops and before the logger closes, on every shutdown path, and a signal-triggered shutdown then exits the process explicitly (0, or 1 if a step never settles within the framework's 10 s ceiling).
192
+
177
193
  ---
178
194
 
179
195
  ## Context
@@ -231,7 +247,7 @@ throw new Error('Invalid query format'); // → ValidationError
231
247
 
232
248
  // McpError — when no factory exists for the code
233
249
  import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
234
- throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
250
+ throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });
235
251
  ```
236
252
 
237
253
  See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
@@ -273,9 +289,9 @@ src/
273
289
 
274
290
  ## Skills
275
291
 
276
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry.
292
+ Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
277
293
 
278
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
294
+ **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
279
295
 
280
296
  Available skills:
281
297
 
@@ -315,7 +331,7 @@ Available skills:
315
331
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
316
332
  | `api-workers` | Cloudflare Workers runtime |
317
333
 
318
- **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
334
+ **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `framework-skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
319
335
 
320
336
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
321
337
 
@@ -331,7 +347,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
331
347
  | `bun run rebuild` | Clean + build |
332
348
  | `bun run clean` | Remove build artifacts |
333
349
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
334
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
350
+ | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
351
+ | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
335
352
  | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
336
353
  | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
337
354
  | `bun run list-skills` | Print the skill registry |
@@ -349,11 +366,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
349
366
 
350
367
  ## Bundling
351
368
 
352
- `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. A server using DataCanvas therefore ships a portable bundle without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so canvas tools report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
369
+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. A server using DataCanvas therefore ships a portable bundle without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so canvas tools report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
353
370
 
354
- **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
371
+ **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
355
372
 
356
- **README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and the `base64` / `encodeURIComponent` config-generation commands are ship-time concerns — run the `polish-docs-meta` skill, which carries the badge format, layout, and generation snippets in `skills/polish-docs-meta/references/readme.md`.
373
+ **README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and the `base64` / `encodeURIComponent` config-generation commands are ship-time concerns — run the `polish-docs-meta` skill, which carries the badge format, layout, and generation snippets in `framework-skills/polish-docs-meta/references/readme.md`.
357
374
 
358
375
  ---
359
376
 
@@ -410,7 +427,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
410
427
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
411
428
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
412
429
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
413
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
414
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
415
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
430
+ - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
431
+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
432
+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
416
433
  - [ ] `npm run devcheck` passes