@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.
- package/AGENTS.md +6 -3
- package/CLAUDE.md +6 -3
- package/README.md +29 -3
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.11.md +113 -0
- package/dist/cli/app-render.d.ts +11 -0
- package/dist/cli/app-render.d.ts.map +1 -0
- package/dist/cli/app-render.js +150 -0
- package/dist/cli/app-render.js.map +1 -0
- package/dist/cli/init.d.ts +2 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +10 -2
- package/dist/cli/init.js.map +1 -1
- package/dist/core/context.d.ts +3 -2
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +25 -16
- package/dist/core/context.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +18 -7
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/services/mirror/sqlite/handle.d.ts +11 -5
- package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/handle.js +46 -8
- package/dist/services/mirror/sqlite/handle.js.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts +6 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
- package/dist/testing/apps/browser.d.ts +27 -0
- package/dist/testing/apps/browser.d.ts.map +1 -0
- package/dist/testing/apps/browser.js +162 -0
- package/dist/testing/apps/browser.js.map +1 -0
- package/dist/testing/apps/cdp-pipe.d.ts +79 -0
- package/dist/testing/apps/cdp-pipe.d.ts.map +1 -0
- package/dist/testing/apps/cdp-pipe.js +200 -0
- package/dist/testing/apps/cdp-pipe.js.map +1 -0
- package/dist/testing/apps/csp.d.ts +17 -0
- package/dist/testing/apps/csp.d.ts.map +1 -0
- package/dist/testing/apps/csp.js +50 -0
- package/dist/testing/apps/csp.js.map +1 -0
- package/dist/testing/apps/host-pages.d.ts +47 -0
- package/dist/testing/apps/host-pages.d.ts.map +1 -0
- package/dist/testing/apps/host-pages.js +138 -0
- package/dist/testing/apps/host-pages.js.map +1 -0
- package/dist/testing/apps/index.d.ts +167 -0
- package/dist/testing/apps/index.d.ts.map +1 -0
- package/dist/testing/apps/index.js +36 -0
- package/dist/testing/apps/index.js.map +1 -0
- package/dist/testing/apps/partial-json.d.ts +14 -0
- package/dist/testing/apps/partial-json.d.ts.map +1 -0
- package/dist/testing/apps/partial-json.js +71 -0
- package/dist/testing/apps/partial-json.js.map +1 -0
- package/dist/testing/apps/run.d.ts +13 -0
- package/dist/testing/apps/run.d.ts.map +1 -0
- package/dist/testing/apps/run.js +659 -0
- package/dist/testing/apps/run.js.map +1 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/logger.d.ts +21 -1
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +31 -14
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +3 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -3
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +16 -13
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +74 -33
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +7 -1
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +7 -1
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +2 -1
- package/framework-skills/add-test/SKILL.md +15 -15
- package/framework-skills/add-tool/SKILL.md +2 -2
- package/framework-skills/api-config/SKILL.md +11 -2
- package/framework-skills/api-context/SKILL.md +2 -2
- package/framework-skills/api-mirror/SKILL.md +4 -3
- package/framework-skills/api-telemetry/SKILL.md +6 -4
- package/framework-skills/api-testing/SKILL.md +5 -1
- package/framework-skills/api-utils/SKILL.md +3 -3
- package/framework-skills/code-simplifier/SKILL.md +2 -2
- package/framework-skills/field-test/SKILL.md +52 -7
- package/framework-skills/git-wrapup/SKILL.md +2 -2
- package/framework-skills/orchestrations/SKILL.md +1 -1
- package/framework-skills/orchestrations/workflows/greenfield-build.md +2 -2
- package/framework-skills/polish-docs-meta/SKILL.md +4 -3
- package/framework-skills/polish-docs-meta/references/server-json.md +24 -26
- package/framework-skills/release-and-publish/SKILL.md +2 -2
- package/framework-skills/release-pr-review/SKILL.md +5 -5
- package/framework-skills/setup/SKILL.md +2 -2
- package/package.json +37 -22
- package/scripts/devcheck.ts +43 -21
- package/scripts/lint-packaging.ts +91 -1
- package/scripts/prune-musl-packages.ts +146 -0
- package/templates/.github/workflows/codeql.yml +10 -1
- package/templates/Dockerfile +13 -4
- package/templates/_.dockerignore +3 -5
- package/templates/_.gitignore +4 -4
- package/templates/package.json +4 -4
- package/templates/server.json +7 -21
package/scripts/devcheck.ts
CHANGED
|
@@ -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
|
|
643
|
-
|
|
644
|
-
//
|
|
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
|
-
'
|
|
680
|
-
'
|
|
681
|
-
'**/.
|
|
682
|
-
'
|
|
683
|
-
'
|
|
684
|
-
'**/*.
|
|
685
|
-
'
|
|
686
|
-
'
|
|
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
|
|
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
|
|
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(
|
|
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),
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
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
|
-
`
|
|
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:
|
|
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 }}
|
package/templates/Dockerfile
CHANGED
|
@@ -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
|
|
42
|
-
# and cross-installs with `--os`/`--cpu`, which pick each
|
|
43
|
-
# optional dependency (native bindings such as DuckDB's) for
|
|
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
|
|
package/templates/_.dockerignore
CHANGED
|
@@ -164,11 +164,9 @@ coverage/
|
|
|
164
164
|
# =============================================================================
|
|
165
165
|
# ENVIRONMENT & CONFIGURATION
|
|
166
166
|
# =============================================================================
|
|
167
|
-
.env
|
|
168
|
-
.
|
|
169
|
-
.
|
|
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.*
|
package/templates/_.gitignore
CHANGED
|
@@ -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
|
-
|
|
64
|
-
.
|
|
61
|
+
.env*
|
|
62
|
+
!.env.example
|
|
63
|
+
!.env.template
|
|
64
|
+
!.env.sample
|
|
65
65
|
.wrangler
|
|
66
66
|
worker-configuration.d.ts
|
|
67
67
|
|
package/templates/package.json
CHANGED
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"zod": "{{ZOD_VERSION}}"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@biomejs/biome": "2.5.
|
|
70
|
+
"@biomejs/biome": "2.5.15",
|
|
71
71
|
"@socketsecurity/bun-security-scanner": "^1.1.3",
|
|
72
|
-
"@types/node": "26.6.
|
|
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.
|
|
77
|
-
"tsc-alias": "^1.9.
|
|
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
|
}
|
package/templates/server.json
CHANGED
|
@@ -12,18 +12,8 @@
|
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
14
14
|
"identifier": "{{PACKAGE_NAME}}",
|
|
15
|
-
"runtimeHint": "
|
|
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": "
|
|
34
|
+
"runtimeHint": "npx",
|
|
45
35
|
"version": "0.1.0",
|
|
46
|
-
"
|
|
36
|
+
"environmentVariables": [
|
|
47
37
|
{
|
|
48
|
-
"
|
|
49
|
-
"
|
|
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.",
|