@cyanheads/mcp-ts-core 0.13.10 → 0.13.11

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 (104) hide show
  1. package/AGENTS.md +6 -3
  2. package/CLAUDE.md +6 -3
  3. package/README.md +29 -3
  4. package/biome.json +1 -1
  5. package/changelog/0.13.x/0.13.11.md +113 -0
  6. package/dist/cli/app-render.d.ts +11 -0
  7. package/dist/cli/app-render.d.ts.map +1 -0
  8. package/dist/cli/app-render.js +150 -0
  9. package/dist/cli/app-render.js.map +1 -0
  10. package/dist/cli/init.d.ts +2 -1
  11. package/dist/cli/init.d.ts.map +1 -1
  12. package/dist/cli/init.js +10 -2
  13. package/dist/cli/init.js.map +1 -1
  14. package/dist/core/context.d.ts +3 -2
  15. package/dist/core/context.d.ts.map +1 -1
  16. package/dist/core/context.js +25 -16
  17. package/dist/core/context.js.map +1 -1
  18. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  19. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +18 -7
  20. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  21. package/dist/services/mirror/sqlite/handle.d.ts +11 -5
  22. package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
  23. package/dist/services/mirror/sqlite/handle.js +46 -8
  24. package/dist/services/mirror/sqlite/handle.js.map +1 -1
  25. package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts +6 -1
  26. package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
  27. package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
  28. package/dist/testing/apps/browser.d.ts +27 -0
  29. package/dist/testing/apps/browser.d.ts.map +1 -0
  30. package/dist/testing/apps/browser.js +162 -0
  31. package/dist/testing/apps/browser.js.map +1 -0
  32. package/dist/testing/apps/cdp-pipe.d.ts +79 -0
  33. package/dist/testing/apps/cdp-pipe.d.ts.map +1 -0
  34. package/dist/testing/apps/cdp-pipe.js +200 -0
  35. package/dist/testing/apps/cdp-pipe.js.map +1 -0
  36. package/dist/testing/apps/csp.d.ts +17 -0
  37. package/dist/testing/apps/csp.d.ts.map +1 -0
  38. package/dist/testing/apps/csp.js +50 -0
  39. package/dist/testing/apps/csp.js.map +1 -0
  40. package/dist/testing/apps/host-pages.d.ts +47 -0
  41. package/dist/testing/apps/host-pages.d.ts.map +1 -0
  42. package/dist/testing/apps/host-pages.js +138 -0
  43. package/dist/testing/apps/host-pages.js.map +1 -0
  44. package/dist/testing/apps/index.d.ts +167 -0
  45. package/dist/testing/apps/index.d.ts.map +1 -0
  46. package/dist/testing/apps/index.js +36 -0
  47. package/dist/testing/apps/index.js.map +1 -0
  48. package/dist/testing/apps/partial-json.d.ts +14 -0
  49. package/dist/testing/apps/partial-json.d.ts.map +1 -0
  50. package/dist/testing/apps/partial-json.js +71 -0
  51. package/dist/testing/apps/partial-json.js.map +1 -0
  52. package/dist/testing/apps/run.d.ts +13 -0
  53. package/dist/testing/apps/run.d.ts.map +1 -0
  54. package/dist/testing/apps/run.js +659 -0
  55. package/dist/testing/apps/run.js.map +1 -0
  56. package/dist/utils/index.d.ts +1 -1
  57. package/dist/utils/index.d.ts.map +1 -1
  58. package/dist/utils/index.js +1 -1
  59. package/dist/utils/index.js.map +1 -1
  60. package/dist/utils/internal/logger.d.ts +21 -1
  61. package/dist/utils/internal/logger.d.ts.map +1 -1
  62. package/dist/utils/internal/logger.js +31 -14
  63. package/dist/utils/internal/logger.js.map +1 -1
  64. package/dist/utils/internal/performance.d.ts +3 -1
  65. package/dist/utils/internal/performance.d.ts.map +1 -1
  66. package/dist/utils/internal/performance.js +8 -3
  67. package/dist/utils/internal/performance.js.map +1 -1
  68. package/dist/utils/security/sanitization.d.ts +16 -13
  69. package/dist/utils/security/sanitization.d.ts.map +1 -1
  70. package/dist/utils/security/sanitization.js +74 -33
  71. package/dist/utils/security/sanitization.js.map +1 -1
  72. package/dist/utils/telemetry/attributes.d.ts +7 -1
  73. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  74. package/dist/utils/telemetry/attributes.js +7 -1
  75. package/dist/utils/telemetry/attributes.js.map +1 -1
  76. package/framework-skills/add-app-tool/SKILL.md +2 -1
  77. package/framework-skills/add-test/SKILL.md +15 -15
  78. package/framework-skills/add-tool/SKILL.md +2 -2
  79. package/framework-skills/api-config/SKILL.md +11 -2
  80. package/framework-skills/api-context/SKILL.md +2 -2
  81. package/framework-skills/api-mirror/SKILL.md +4 -3
  82. package/framework-skills/api-telemetry/SKILL.md +6 -4
  83. package/framework-skills/api-testing/SKILL.md +5 -1
  84. package/framework-skills/api-utils/SKILL.md +3 -3
  85. package/framework-skills/code-simplifier/SKILL.md +2 -2
  86. package/framework-skills/field-test/SKILL.md +52 -7
  87. package/framework-skills/git-wrapup/SKILL.md +2 -2
  88. package/framework-skills/orchestrations/SKILL.md +1 -1
  89. package/framework-skills/orchestrations/workflows/greenfield-build.md +2 -2
  90. package/framework-skills/polish-docs-meta/SKILL.md +4 -3
  91. package/framework-skills/polish-docs-meta/references/server-json.md +24 -26
  92. package/framework-skills/release-and-publish/SKILL.md +2 -2
  93. package/framework-skills/release-pr-review/SKILL.md +5 -5
  94. package/framework-skills/setup/SKILL.md +2 -2
  95. package/package.json +37 -22
  96. package/scripts/devcheck.ts +43 -21
  97. package/scripts/lint-packaging.ts +91 -1
  98. package/scripts/prune-musl-packages.ts +146 -0
  99. package/templates/.github/workflows/codeql.yml +10 -1
  100. package/templates/Dockerfile +13 -4
  101. package/templates/_.dockerignore +3 -5
  102. package/templates/_.gitignore +4 -4
  103. package/templates/package.json +4 -4
  104. package/templates/server.json +7 -21
@@ -639,15 +639,25 @@ const ALL_CHECKS: Check[] = [
639
639
  canFix: false,
640
640
  getCommand: (ctx) => {
641
641
  if (!isGitRepo()) return null; // no repo to grep — fresh scaffold before `git init`
642
- // git grep -n (line number) -E (extended regex) -i (case-insensitive)
643
- const baseCmd = ['git', 'grep', '-nEi', '\\b(TODO|FIXME)\\b'];
644
- // Exclude files where TODO/FIXME appears as prose or intentional stubs
642
+ // -w is git's own whole-word test: macOS's regex library reads `\b` in an
643
+ // extended regex as a literal `b`. Case-sensitive, so a lowercase `todo`
644
+ // variable or stop word is not a marker.
645
+ const baseCmd = ['git', 'grep', '-nwE', '(TODO|FIXME)'];
646
+ // Exclude files where TODO/FIXME appears as prose or intentional stubs:
647
+ // changelogs, tests, this script, and the framework's skill prose wherever
648
+ // the maintenance sync copies it.
645
649
  const excludes = [
646
650
  ':!CHANGELOG.md',
647
651
  ':!changelog/',
648
652
  ':!*.lock',
649
653
  ':!scripts/devcheck.ts',
650
654
  ':!tests/',
655
+ ':!framework-skills/',
656
+ ':!.claude/skills/',
657
+ ':!.agents/skills/',
658
+ ':!.codex/skills/',
659
+ ':!.cursor/skills/',
660
+ ':!.windsurf/skills/',
651
661
  ];
652
662
  if (ctx.isHuskyHook && ctx.stagedFiles.length > 0) {
653
663
  // Check only staged files in the working tree
@@ -670,28 +680,38 @@ const ALL_CHECKS: Check[] = [
670
680
  name: 'Tracked Secrets',
671
681
  flag: '--no-secrets',
672
682
  canFix: false,
673
- // Check if common sensitive files are tracked by git.
683
+ // Check if common sensitive files are tracked by git. `:(glob)` makes `**/`
684
+ // match zero or more directories, so each shape is caught at the repository
685
+ // root too; under it `*` stops at `/`, so the two name-prefix shapes carry a
686
+ // `/**` twin for the files inside a directory of that name (`secrets/`,
687
+ // `.env.d/`).
674
688
  getCommand: () => {
675
689
  if (!isGitRepo()) return null; // no repo — `git ls-files` would exit 128, not a finding
676
690
  return [
677
691
  'git',
678
692
  'ls-files',
679
- '*.env*',
680
- '**/.npmrc',
681
- '**/.netrc',
682
- '**/credentials.json',
683
- '**/*.pem',
684
- '**/*.key',
685
- '**/secret*',
686
- '**/.htpasswd',
693
+ ':(glob)**/*.env*',
694
+ ':(glob)**/*.env*/**',
695
+ ':(glob)**/.npmrc',
696
+ ':(glob)**/.netrc',
697
+ ':(glob)**/credentials.json',
698
+ ':(glob)**/*.pem',
699
+ ':(glob)**/*.key',
700
+ ':(glob)**/secret*',
701
+ ':(glob)**/secret*/**',
702
+ ':(glob)**/.htpasswd',
687
703
  ];
688
704
  },
689
- // Success if output is empty OR only contains safe patterns.
705
+ // Success if output is empty OR only contains safe files: `.env` templates
706
+ // anywhere, and GitHub's secret scanning config at the one path it reads.
690
707
  isSuccess: (result, _mode) => {
691
708
  if (result.exitCode !== 0) return false;
692
- const SAFE_PATTERNS = ['.env.example', '.env.template', '.env.sample'];
709
+ const SAFE_SUFFIXES = ['.env.example', '.env.template', '.env.sample'];
710
+ const SAFE_PATHS = ['.github/secret_scanning.yml'];
693
711
  const files = result.stdout.trim().split('\n').filter(Boolean);
694
- const dangerous = files.filter((f) => !SAFE_PATTERNS.some((safe) => f.endsWith(safe)));
712
+ const dangerous = files.filter(
713
+ (f) => !SAFE_PATHS.includes(f) && !SAFE_SUFFIXES.some((safe) => f.endsWith(safe)),
714
+ );
695
715
  return dangerous.length === 0;
696
716
  },
697
717
  tip: (c) =>
@@ -712,11 +732,12 @@ const ALL_CHECKS: Check[] = [
712
732
  // Validates env var alignment between manifest.json (MCPB bundle) and
713
733
  // server.json (MCP Registry), plus plugin marketplace manifests (#240), the
714
734
  // bundle-content guards on .mcpbignore (#343), the README version badge
715
- // (#418), and the Dockerfile build platform. Runs when any of those inputs
716
- // is present; skipped cleanly when none exist — consumers on an HTTP-only
717
- // deploy are unaffected. README.md and Dockerfile are triggers in their own
718
- // right: each check must gate a project that carries no bundle or plugin
719
- // metadata at all, which the other inputs only covered incidentally.
735
+ // (#418), the Dockerfile build platform, and the launch shape of server.json
736
+ // npm entries (#622). Runs when any of those inputs is present; skipped
737
+ // cleanly when none exist — consumers on an HTTP-only deploy are unaffected.
738
+ // README.md, Dockerfile, and server.json are triggers in their own right:
739
+ // each check must gate a project that carries no bundle or plugin metadata
740
+ // at all, which the other inputs only covered incidentally.
720
741
  getCommand: () => {
721
742
  const inputs = [
722
743
  'manifest.json',
@@ -726,12 +747,13 @@ const ALL_CHECKS: Check[] = [
726
747
  '.mcpbignore',
727
748
  'README.md',
728
749
  'Dockerfile',
750
+ 'server.json',
729
751
  ];
730
752
  if (!inputs.some((input) => existsSync(path.join(ROOT_DIR, input)))) return null;
731
753
  return ['bun', 'run', 'scripts/lint-packaging.ts'];
732
754
  },
733
755
  tip: (c) =>
734
- `Align env var names between ${c.bold('manifest.json')} ${c.bold('mcp_config.env')} and ${c.bold('server.json')} stdio package ${c.bold('environmentVariables[]')}.`,
756
+ `Each ${c.bold('✗')} line above names the file and the change that fixes it; ${c.bold('scripts/lint-packaging.ts')} documents every check.`,
735
757
  },
736
758
  {
737
759
  name: 'Framework Antipatterns',
@@ -80,6 +80,15 @@
80
80
  * npm and the MCP Registry. `HEALTHCHECK`/`CMD`/`ENTRYPOINT` run at
81
81
  * container start and never count. One error per stage, naming each
82
82
  * offending line (issue #575). Skipped when there is no `Dockerfile`.
83
+ * 16. server.json npm launch shape: a registry client runs an npm entry as
84
+ * `npx <identifier>@<version> <packageArguments>` with the entry's
85
+ * `environmentVariables`, so the package's bin starts, never an npm
86
+ * script. No npm entry may carry the positional `run` + `start:*` pair
87
+ * (npm-script syntax the bin receives as ignored argv), and a
88
+ * `streamable-http` npm entry must fix `MCP_TRANSPORT_TYPE` as
89
+ * `"value": "http"` — absent, or only a user-editable `default`, the
90
+ * server starts on stdio. Runs with or without `manifest.json`; skipped
91
+ * when there is no `server.json` (issue #622).
83
92
  *
84
93
  * Every check skips cleanly when its input is absent — consumers who deleted
85
94
  * `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
@@ -990,6 +999,82 @@ export function checkDockerfileBuildPlatform(dockerfile: string): string[] {
990
999
  );
991
1000
  }
992
1001
 
1002
+ /** The environment entry a `streamable-http` npm package carries, as the mcp-ts-core scaffold writes it. */
1003
+ const HTTP_TRANSPORT_ENV =
1004
+ '{ "name": "MCP_TRANSPORT_TYPE", "description": "Selects the HTTP transport.", "format": "string", "value": "http" }';
1005
+
1006
+ /** An npm script name the `run` positional selects: `start` or `start:<variant>`. */
1007
+ const NPM_START_SCRIPT = /^start(?::|$)/;
1008
+
1009
+ /** The `value` of a positional package argument; undefined for anything else. */
1010
+ function positionalValue(arg: unknown): string | undefined {
1011
+ return isRecord(arg) && arg.type === 'positional' && typeof arg.value === 'string'
1012
+ ? arg.value
1013
+ : undefined;
1014
+ }
1015
+
1016
+ /**
1017
+ * Check 16: the launch shape of `server.json` npm entries. A registry client
1018
+ * runs an npm entry as `npx <identifier>@<version> <packageArguments>` with the
1019
+ * entry's `environmentVariables`, so the package's bin starts, never an npm
1020
+ * script. A positional `run` followed by `start` or `start:<variant>` is
1021
+ * npm-script syntax the bin receives as argv and ignores. The transport comes
1022
+ * only from `MCP_TRANSPORT_TYPE`, so a `streamable-http` npm entry must fix it
1023
+ * as `"value": "http"`: absent, it starts on stdio, and a `default` is
1024
+ * user-editable. Entries of other registry types are left alone. Every error
1025
+ * names the entry's index and the change that fixes it.
1026
+ */
1027
+ export function checkServerJsonLaunch(serverJson: unknown): string[] {
1028
+ const errors: string[] = [];
1029
+ const packages =
1030
+ isRecord(serverJson) && Array.isArray(serverJson.packages) ? serverJson.packages : [];
1031
+
1032
+ for (const [index, entry] of packages.entries()) {
1033
+ if (!isRecord(entry) || entry.registryType !== 'npm') continue;
1034
+ const label = `server.json packages[${index}]`;
1035
+
1036
+ const args: unknown[] = Array.isArray(entry.packageArguments) ? entry.packageArguments : [];
1037
+ const runAt = args.findIndex(
1038
+ (arg, i) =>
1039
+ positionalValue(arg) === 'run' && NPM_START_SCRIPT.test(positionalValue(args[i + 1]) ?? ''),
1040
+ );
1041
+ if (runAt !== -1) {
1042
+ errors.push(
1043
+ `${label} packageArguments carry "run" "${positionalValue(args[runAt + 1])}" — npm-script ` +
1044
+ 'syntax: a registry client launches the entry as `npx <identifier>@<version> <packageArguments>`, ' +
1045
+ "so the package's bin receives both as argv it ignores; remove both arguments",
1046
+ );
1047
+ }
1048
+
1049
+ if (!isRecord(entry.transport) || entry.transport.type !== 'streamable-http') continue;
1050
+ const envVars: unknown[] = Array.isArray(entry.environmentVariables)
1051
+ ? entry.environmentVariables
1052
+ : [];
1053
+ const transportVar = envVars.find((v) => isRecord(v) && v.name === 'MCP_TRANSPORT_TYPE');
1054
+ if (!isRecord(transportVar)) {
1055
+ errors.push(
1056
+ `${label} (streamable-http) does not set MCP_TRANSPORT_TYPE, so the server it launches ` +
1057
+ `starts on stdio; add ${HTTP_TRANSPORT_ENV} to its environmentVariables`,
1058
+ );
1059
+ } else if (transportVar.value === undefined) {
1060
+ const reason =
1061
+ transportVar.default === 'http'
1062
+ ? '"default": "http" is user-editable, so the HTTP transport is not guaranteed'
1063
+ : 'the server it launches starts on stdio';
1064
+ errors.push(
1065
+ `${label} (streamable-http) MCP_TRANSPORT_TYPE has no "value" — ${reason}; set "value": "http"`,
1066
+ );
1067
+ } else if (transportVar.value !== 'http') {
1068
+ errors.push(
1069
+ `${label} (streamable-http) MCP_TRANSPORT_TYPE "value" is "${String(transportVar.value)}" — ` +
1070
+ 'set it to "http"',
1071
+ );
1072
+ }
1073
+ }
1074
+
1075
+ return errors;
1076
+ }
1077
+
993
1078
  /** Read `packaging.pluginManifests` from devcheck.config.json; default on. */
994
1079
  function pluginManifestsEnabled(): boolean {
995
1080
  const cfg = tryReadJson<{ packaging?: { pluginManifests?: boolean } }>(
@@ -1007,6 +1092,7 @@ async function main(): Promise<void> {
1007
1092
  resolve('package.json'),
1008
1093
  );
1009
1094
  const unscopedName = pkg?.name?.split('/').pop();
1095
+ const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
1010
1096
 
1011
1097
  // ── Manifest-dependent checks (1–4 + manifest identity) ──
1012
1098
  const manifestPath = resolve('manifest.json');
@@ -1039,7 +1125,6 @@ async function main(): Promise<void> {
1039
1125
 
1040
1126
  errors.push(...checkManifestUserConfigWiring(manifest));
1041
1127
 
1042
- const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
1043
1128
  if (serverJson) {
1044
1129
  const manifestEnv = manifest.server?.mcp_config?.env ?? {};
1045
1130
  const manifestEnvKeys = new Set(Object.keys(manifestEnv));
@@ -1083,6 +1168,11 @@ async function main(): Promise<void> {
1083
1168
  notes.push('No manifest.json — skipping manifest/server.json alignment checks.');
1084
1169
  }
1085
1170
 
1171
+ // ── server.json npm launch shape (check 16) ──
1172
+ if (serverJson) {
1173
+ errors.push(...checkServerJsonLaunch(serverJson));
1174
+ }
1175
+
1086
1176
  // ── Bundle-content guard (checks 5–7) ──
1087
1177
  const mcpbignorePath = resolve('.mcpbignore');
1088
1178
  if (existsSync(mcpbignorePath)) {
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * @fileoverview Deletes musl-only packages from a production `node_modules` —
4
+ * the Dockerfile `deps` stage's step after the OTel install.
5
+ *
6
+ * `bun install --os/--cpu` has no libc filter, so an optional dependency
7
+ * published in glibc and musl variants installs both: DuckDB's native
8
+ * bindings add a ~70 MB musl copy per image. The runtime image
9
+ * (`oven/bun:<version>-slim`) is Debian, glibc, and never loads it. Every
10
+ * installed package whose own `package.json` `libc` lists `musl` and not
11
+ * `glibc` is deleted, at any depth — hoisted, scoped, nested `node_modules`,
12
+ * and Bun's isolated `node_modules/.bun` store — along with each symlink that
13
+ * pointed into it. The manifest decides, never the package name. A later
14
+ * `bun install` restores what this removes, so it runs after every install.
15
+ * A server whose runtime image is musl-based (Alpine) drops the step.
16
+ *
17
+ * @example
18
+ * // In the Dockerfile deps stage, after the OTel step:
19
+ * // bun scripts/prune-musl-packages.ts
20
+ * @module scripts/prune-musl-packages
21
+ */
22
+ import { existsSync, readdirSync, readFileSync, readlinkSync, rmSync } from 'node:fs';
23
+ import { dirname, join, relative, resolve, sep } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ interface Manifest {
27
+ libc?: string | string[];
28
+ name: string;
29
+ version: string;
30
+ }
31
+
32
+ /** An installed package the prune deleted. */
33
+ export interface PrunedPackage {
34
+ /** Absolute path of the deleted package directory. */
35
+ dir: string;
36
+ name: string;
37
+ version: string;
38
+ }
39
+
40
+ /** What one prune deleted. */
41
+ export interface PruneResult {
42
+ removed: PrunedPackage[];
43
+ /** Absolute paths of the symlinks removed because they pointed into a deleted package. */
44
+ unlinked: string[];
45
+ }
46
+
47
+ /** True when a manifest's `libc` admits musl and not glibc — a package a glibc runtime cannot load. */
48
+ export function isMuslOnly(manifest: Pick<Manifest, 'libc'>): boolean {
49
+ const libc = [manifest.libc ?? []].flat();
50
+ return libc.includes('musl') && !libc.includes('glibc');
51
+ }
52
+
53
+ /**
54
+ * Deletes every musl-only package under `nodeModules`, plus the symlinks into
55
+ * them. Symlinks are never followed, so each package is visited once, at its
56
+ * real directory.
57
+ */
58
+ export function pruneMuslPackages(nodeModules: string): PruneResult {
59
+ const removed: PrunedPackage[] = [];
60
+ const links: string[] = [];
61
+
62
+ const visitPackage = (dir: string): void => {
63
+ const manifestPath = join(dir, 'package.json');
64
+ if (!existsSync(manifestPath)) return;
65
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Manifest;
66
+ if (isMuslOnly(manifest)) {
67
+ removed.push({ dir, name: manifest.name, version: manifest.version });
68
+ return;
69
+ }
70
+ const nested = join(dir, 'node_modules');
71
+ if (existsSync(nested)) scanNodeModules(nested);
72
+ };
73
+
74
+ /** Visits each package position of a `node_modules` directory: `<name>` and `@scope/<name>`. */
75
+ const scanNodeModules = (dir: string): void => {
76
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
77
+ const path = join(dir, entry.name);
78
+ if (entry.isSymbolicLink()) links.push(path);
79
+ else if (!entry.isDirectory()) continue;
80
+ else if (entry.name === '.bun') scanBunStore(path);
81
+ else if (entry.name.startsWith('@')) {
82
+ for (const scoped of readdirSync(path, { withFileTypes: true })) {
83
+ const scopedPath = join(path, scoped.name);
84
+ if (scoped.isSymbolicLink()) links.push(scopedPath);
85
+ else if (scoped.isDirectory()) visitPackage(scopedPath);
86
+ }
87
+ } else if (!entry.name.startsWith('.')) visitPackage(path);
88
+ }
89
+ };
90
+
91
+ /** Bun's isolated store: `.bun/<entry>/node_modules` per package, plus the hoisted `.bun/node_modules`. */
92
+ const scanBunStore = (store: string): void => {
93
+ for (const entry of readdirSync(store, { withFileTypes: true })) {
94
+ if (!entry.isDirectory()) continue;
95
+ const modules =
96
+ entry.name === 'node_modules'
97
+ ? join(store, entry.name)
98
+ : join(store, entry.name, 'node_modules');
99
+ if (existsSync(modules)) scanNodeModules(modules);
100
+ }
101
+ };
102
+
103
+ scanNodeModules(nodeModules);
104
+
105
+ const insideRemoved = (target: string): boolean =>
106
+ removed.some(({ dir }) => target === dir || target.startsWith(`${dir}${sep}`));
107
+ const unlinked = links.filter((path) =>
108
+ insideRemoved(resolve(dirname(path), readlinkSync(path))),
109
+ );
110
+ for (const { dir } of removed) rmSync(dir, { recursive: true, force: true });
111
+ for (const path of unlinked) rmSync(path);
112
+ return { removed, unlinked };
113
+ }
114
+
115
+ function main(): number {
116
+ const nodeModules = join(process.cwd(), 'node_modules');
117
+ if (!existsSync(nodeModules)) {
118
+ console.error(
119
+ `prune-musl-packages: ${nodeModules} does not exist — run it from the project root after bun install.`,
120
+ );
121
+ return 1;
122
+ }
123
+
124
+ const { removed, unlinked } = pruneMuslPackages(nodeModules);
125
+ if (removed.length === 0) {
126
+ console.log('prune-musl-packages: no musl-only packages in node_modules.');
127
+ return 0;
128
+ }
129
+ const plural = (count: number, noun: string) => `${count} ${noun}${count === 1 ? '' : 's'}`;
130
+ console.log(
131
+ `prune-musl-packages: removed ${plural(removed.length, 'musl-only package')} a glibc runtime cannot load:`,
132
+ );
133
+ for (const { dir, name, version } of removed) {
134
+ console.log(` ${name}@${version} (${relative(process.cwd(), dir)})`);
135
+ }
136
+ if (unlinked.length > 0) {
137
+ console.log(
138
+ `prune-musl-packages: removed ${plural(unlinked.length, 'symlink')} into those packages.`,
139
+ );
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
145
+ process.exit(main());
146
+ }
@@ -2,6 +2,9 @@
2
2
  # Verification (typecheck, lint, tests) is local; this file exists because CodeQL
3
3
  # is GitHub-owned end to end and a workflow file is visible in the repo where a
4
4
  # repo-level "default setup" is not. Default setup must be OFF for this to run.
5
+ # Each language uploads under `/language:<language>`, default setup's category.
6
+ # GitHub re-evaluates an alert only through analyses in the alert's own
7
+ # category, so alerts default setup raised close here once the code is fixed.
5
8
  name: CodeQL
6
9
 
7
10
  on:
@@ -24,6 +27,10 @@ jobs:
24
27
  security-events: write
25
28
  contents: read
26
29
  actions: read
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ language: [actions, javascript-typescript]
27
34
 
28
35
  steps:
29
36
  - name: Checkout repository
@@ -32,8 +39,10 @@ jobs:
32
39
  - name: Initialize CodeQL
33
40
  uses: github/codeql-action/init@v4
34
41
  with:
35
- languages: javascript-typescript, actions
42
+ languages: ${{ matrix.language }}
36
43
  build-mode: none
37
44
 
38
45
  - name: Perform CodeQL Analysis
39
46
  uses: github/codeql-action/analyze@v4
47
+ with:
48
+ category: /language:${{ matrix.language }}
@@ -38,10 +38,10 @@ RUN bun run build
38
38
  #
39
39
  # Installs the production dependency tree for the target platform. Every step
40
40
  # here can run JavaScript — bunfig.toml's security scanner runs as a Bun
41
- # program, and so does the OTel script — so the stage runs on $BUILDPLATFORM
42
- # and cross-installs with `--os`/`--cpu`, which pick each platform-specific
43
- # optional dependency (native bindings such as DuckDB's) for the target. Only
44
- # `node_modules` leaves this stage.
41
+ # program, and so do the OTel and musl-prune scripts — so the stage runs on
42
+ # $BUILDPLATFORM and cross-installs with `--os`/`--cpu`, which pick each
43
+ # platform-specific optional dependency (native bindings such as DuckDB's) for
44
+ # the target. Only `node_modules` leaves this stage.
45
45
  #
46
46
  # A clean image rather than `FROM build`: the build stage's node_modules holds
47
47
  # devDependencies.
@@ -93,6 +93,15 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
93
93
  bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
94
94
  fi
95
95
 
96
+ # `--os`/`--cpu` have no libc counterpart, so a native dependency published in
97
+ # glibc and musl variants (DuckDB's bindings, for one) installs both. The
98
+ # runtime image is Debian (glibc) and never loads the musl copy; the script
99
+ # deletes every package whose own `libc` admits only musl. It follows every
100
+ # install, since a later `bun install` restores what it removes. A server on a
101
+ # musl (Alpine) runtime image drops these two lines.
102
+ COPY scripts/prune-musl-packages.ts ./scripts/
103
+ RUN bun scripts/prune-musl-packages.ts
104
+
96
105
  # The seeded scanner served only the installs above; keep it out of the image.
97
106
  RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
98
107
 
@@ -164,11 +164,9 @@ coverage/
164
164
  # =============================================================================
165
165
  # ENVIRONMENT & CONFIGURATION
166
166
  # =============================================================================
167
- .env
168
- .env.local
169
- .env.development.local
170
- .env.test.local
171
- .env.production.local
167
+ .env*
168
+ .mcpregistry_github_token
169
+ .mcpregistry_registry_token
172
170
  .sample-env
173
171
  sample.*
174
172
  !sample.template.*
@@ -40,7 +40,6 @@ dist/
40
40
  # TESTING & COVERAGE
41
41
  # =============================================================================
42
42
  coverage/
43
- !coverage/coverage-final.json
44
43
 
45
44
  # =============================================================================
46
45
  # LOGS & DATABASES
@@ -59,9 +58,10 @@ logs/
59
58
  # =============================================================================
60
59
  # ENVIRONMENT & CONFIGURATION
61
60
  # =============================================================================
62
- .env
63
- .env.local
64
- .env.*.local
61
+ .env*
62
+ !.env.example
63
+ !.env.template
64
+ !.env.sample
65
65
  .wrangler
66
66
  worker-configuration.d.ts
67
67
 
@@ -67,14 +67,14 @@
67
67
  "zod": "{{ZOD_VERSION}}"
68
68
  },
69
69
  "devDependencies": {
70
- "@biomejs/biome": "2.5.14",
70
+ "@biomejs/biome": "2.5.15",
71
71
  "@socketsecurity/bun-security-scanner": "^1.1.3",
72
- "@types/node": "26.6.2",
72
+ "@types/node": "26.6.3",
73
73
  "@vitest/coverage-istanbul": "4.1.11",
74
74
  "depcheck": "^1.4.7",
75
75
  "fast-check": "^4.9.0",
76
- "ignore": "^7.0.9",
77
- "tsc-alias": "^1.9.5",
76
+ "ignore": "^7.0.11",
77
+ "tsc-alias": "^1.9.7",
78
78
  "typescript": "^7.0.2",
79
79
  "vitest": "^4.1.11"
80
80
  }
@@ -12,18 +12,8 @@
12
12
  "registryType": "npm",
13
13
  "registryBaseUrl": "https://registry.npmjs.org",
14
14
  "identifier": "{{PACKAGE_NAME}}",
15
- "runtimeHint": "node",
15
+ "runtimeHint": "npx",
16
16
  "version": "0.1.0",
17
- "packageArguments": [
18
- {
19
- "type": "positional",
20
- "value": "run"
21
- },
22
- {
23
- "type": "positional",
24
- "value": "start:stdio"
25
- }
26
- ],
27
17
  "environmentVariables": [
28
18
  {
29
19
  "name": "MCP_LOG_LEVEL",
@@ -41,19 +31,15 @@
41
31
  "registryType": "npm",
42
32
  "registryBaseUrl": "https://registry.npmjs.org",
43
33
  "identifier": "{{PACKAGE_NAME}}",
44
- "runtimeHint": "node",
34
+ "runtimeHint": "npx",
45
35
  "version": "0.1.0",
46
- "packageArguments": [
36
+ "environmentVariables": [
47
37
  {
48
- "type": "positional",
49
- "value": "run"
38
+ "name": "MCP_TRANSPORT_TYPE",
39
+ "description": "Selects the HTTP transport.",
40
+ "format": "string",
41
+ "value": "http"
50
42
  },
51
- {
52
- "type": "positional",
53
- "value": "start:http"
54
- }
55
- ],
56
- "environmentVariables": [
57
43
  {
58
44
  "name": "MCP_HTTP_HOST",
59
45
  "description": "The hostname for the HTTP server.",