@cyanheads/mcp-ts-core 0.13.9 → 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 +41 -15
- package/CLAUDE.md +41 -15
- package/README.md +37 -11
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.10.md +118 -0
- 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/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +15 -1
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +92 -22
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +65 -16
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/worker.d.ts +6 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/error-contract-rules.d.ts +3 -44
- package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
- package/dist/linter/rules/error-contract-rules.js +8 -144
- package/dist/linter/rules/error-contract-rules.js.map +1 -1
- package/dist/linter/rules/index.d.ts +1 -1
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -1
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +1 -2
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +1 -2
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +26 -13
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +32 -17
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +120 -8
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +177 -12
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts +10 -2
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +49 -11
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +4 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js +6 -4
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +4 -3
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +60 -21
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/server.d.ts +9 -0
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +14 -13
- package/dist/mcp-server/server.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +7 -3
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -5
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +17 -9
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +91 -50
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +7 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.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/testing/index.d.ts +17 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -7
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -15
- package/dist/types-global/errors.d.ts.map +1 -1
- 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/error-handler/errorHandler.d.ts +5 -4
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +9 -7
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +3 -1
- package/dist/utils/internal/error-handler/types.d.ts.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 +7 -3
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +16 -9
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/telemetryMessages.d.ts +0 -1
- package/dist/utils/internal/telemetryMessages.d.ts.map +1 -1
- package/dist/utils/internal/telemetryMessages.js +0 -1
- package/dist/utils/internal/telemetryMessages.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 +12 -5
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +12 -5
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +2 -1
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +20 -17
- package/framework-skills/add-tool/SKILL.md +30 -34
- package/framework-skills/api-config/SKILL.md +12 -2
- package/framework-skills/api-context/SKILL.md +156 -41
- package/framework-skills/api-errors/SKILL.md +43 -47
- package/framework-skills/api-linter/SKILL.md +5 -29
- package/framework-skills/api-mirror/SKILL.md +4 -3
- package/framework-skills/api-telemetry/SKILL.md +16 -10
- package/framework-skills/api-testing/SKILL.md +47 -11
- package/framework-skills/api-utils/SKILL.md +3 -3
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/code-simplifier/SKILL.md +2 -2
- package/framework-skills/design-mcp-server/SKILL.md +5 -5
- package/framework-skills/field-test/SKILL.md +53 -8
- 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 +3 -3
- package/framework-skills/release-pr-review/SKILL.md +6 -6
- package/framework-skills/security-pass/SKILL.md +8 -7
- package/framework-skills/setup/SKILL.md +2 -2
- package/package.json +38 -22
- package/scripts/devcheck.ts +43 -21
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-packaging.ts +279 -28
- package/scripts/prune-musl-packages.ts +146 -0
- package/templates/.env.example +2 -0
- package/templates/.github/workflows/codeql.yml +10 -1
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +76 -50
- package/templates/_.dockerignore +3 -5
- package/templates/_.gitignore +4 -4
- package/templates/package.json +4 -4
- package/templates/server.json +7 -21
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
|
@@ -69,12 +69,26 @@
|
|
|
69
69
|
* same rule check 10 applies to the plugin manifests — the bundle's install
|
|
70
70
|
* dialog shows it. Skipped when `manifest.json` or the package version is
|
|
71
71
|
* absent.
|
|
72
|
-
* 15. Dockerfile build platform:
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
72
|
+
* 15. Dockerfile build platform: a stage that does not start
|
|
73
|
+
* `FROM --platform=$BUILDPLATFORM` must not run JavaScript while it
|
|
74
|
+
* builds — no `RUN` invoking `bun` for anything but `install`/`add`
|
|
75
|
+
* (`bun run build`, `bun -e`, a script, `bunx`), and no `bun install`/
|
|
76
|
+
* `bun add` once `bunfig.toml` is in the stage, since its security scanner
|
|
77
|
+
* runs as a Bun program. The non-native leg of a multi-arch `docker buildx`
|
|
78
|
+
* build runs such a stage under QEMU, where bun >= 1.4 aborts and no image
|
|
79
|
+
* publishes for either architecture — and the image publishes last, after
|
|
80
|
+
* npm and the MCP Registry. `HEALTHCHECK`/`CMD`/`ENTRYPOINT` run at
|
|
81
|
+
* container start and never count. One error per stage, naming each
|
|
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).
|
|
78
92
|
*
|
|
79
93
|
* Every check skips cleanly when its input is absent — consumers who deleted
|
|
80
94
|
* `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
|
|
@@ -183,10 +197,10 @@ function tryReadJson<T>(path: string): T | undefined {
|
|
|
183
197
|
* to evaluate which paths survive the ignore rules. Returns an array of error
|
|
184
198
|
* strings; empty means all checks passed.
|
|
185
199
|
*
|
|
186
|
-
* **Context note:**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
200
|
+
* **Context note:** `ignore` is a devDependency of the framework and of every
|
|
201
|
+
* scaffold (`templates/package.json`), so it resolves wherever
|
|
202
|
+
* `bun run lint:packaging` runs against an `.mcpbignore` — a server project, or
|
|
203
|
+
* mcp-ts-core itself. Where it cannot load, the guard is skipped.
|
|
190
204
|
*/
|
|
191
205
|
interface IgnoreMatcher {
|
|
192
206
|
add(patterns: string[]): IgnoreMatcher;
|
|
@@ -799,36 +813,268 @@ export function checkManifestVersion(manifest: Manifest, packageVersion?: string
|
|
|
799
813
|
];
|
|
800
814
|
}
|
|
801
815
|
|
|
802
|
-
|
|
816
|
+
/** One Dockerfile instruction: continuations joined, a `RUN` heredoc body folded in. */
|
|
817
|
+
interface DockerInstruction {
|
|
818
|
+
args: string;
|
|
819
|
+
keyword: string;
|
|
820
|
+
/** 1-based line the instruction starts on. */
|
|
821
|
+
line: number;
|
|
822
|
+
text: string;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const DOCKERFILE_SKIPPED_LINE = /^\s*(?:#|$)/;
|
|
826
|
+
const DOCKERFILE_CONTINUATION = /\\\s*$/;
|
|
827
|
+
const DOCKERFILE_HEREDOC = /<<-?(["']?)([A-Za-z_]\w*)\1/g;
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Splits a Dockerfile into instructions the way BuildKit reads it: a trailing
|
|
831
|
+
* `\` continues onto the next line, comment and blank lines inside a
|
|
832
|
+
* continuation are dropped, and a `RUN` heredoc's body belongs to its `RUN`.
|
|
833
|
+
*/
|
|
834
|
+
function dockerInstructions(dockerfile: string): DockerInstruction[] {
|
|
835
|
+
const lines = dockerfile.split(/\r?\n/);
|
|
836
|
+
const instructions: DockerInstruction[] = [];
|
|
837
|
+
for (let i = 0; i < lines.length; i++) {
|
|
838
|
+
if (DOCKERFILE_SKIPPED_LINE.test(lines[i] ?? '')) continue;
|
|
839
|
+
const line = i + 1;
|
|
840
|
+
let text = (lines[i] ?? '').trim();
|
|
841
|
+
while (DOCKERFILE_CONTINUATION.test(text) && i + 1 < lines.length) {
|
|
842
|
+
text = text.replace(DOCKERFILE_CONTINUATION, ' ');
|
|
843
|
+
do i++;
|
|
844
|
+
while (i < lines.length && DOCKERFILE_SKIPPED_LINE.test(lines[i] ?? ''));
|
|
845
|
+
text += (lines[i] ?? '').trim();
|
|
846
|
+
}
|
|
847
|
+
const keyword = (text.split(/\s/, 1)[0] ?? '').toUpperCase();
|
|
848
|
+
if (keyword === 'RUN') {
|
|
849
|
+
for (const [, , delimiter] of text.matchAll(DOCKERFILE_HEREDOC)) {
|
|
850
|
+
while (i + 1 < lines.length && (lines[++i] ?? '').trim() !== delimiter) {
|
|
851
|
+
text += `\n${lines[i]}`;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
instructions.push({ keyword, args: text.slice(keyword.length).trim(), line, text });
|
|
856
|
+
}
|
|
857
|
+
return instructions;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Words that may precede the command itself in a simple shell command. */
|
|
861
|
+
const SHELL_COMMAND_PREFIX = new Set([
|
|
862
|
+
'!',
|
|
863
|
+
'command',
|
|
864
|
+
'do',
|
|
865
|
+
'elif',
|
|
866
|
+
'else',
|
|
867
|
+
'env',
|
|
868
|
+
'exec',
|
|
869
|
+
'if',
|
|
870
|
+
'then',
|
|
871
|
+
'time',
|
|
872
|
+
'until',
|
|
873
|
+
'while',
|
|
874
|
+
]);
|
|
875
|
+
|
|
876
|
+
/** A `bun`/`bunx` invocation in a `RUN`: the command word and the words after it. */
|
|
877
|
+
interface BunInvocation {
|
|
878
|
+
args: string[];
|
|
879
|
+
command: 'bun' | 'bunx';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function bunCommand(word: string | undefined): BunInvocation['command'] | undefined {
|
|
883
|
+
const name = word?.split('/').pop();
|
|
884
|
+
return name === 'bun' || name === 'bunx' ? name : undefined;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* The `bun`/`bunx` invocations a `RUN` instruction's command makes, read from
|
|
889
|
+
* command position only — `chown bun:bun`, `/root/.bun/`, and a quoted
|
|
890
|
+
* mention are not invocations. Quoted strings are opaque, so a `bun -e '…'`
|
|
891
|
+
* program never splits into commands of its own.
|
|
892
|
+
*/
|
|
893
|
+
function bunInvocations(runArgs: string): BunInvocation[] {
|
|
894
|
+
const command = runArgs.replace(/^(?:--[\w-]+=\S+\s+)*/, '');
|
|
895
|
+
if (command.startsWith('[')) {
|
|
896
|
+
try {
|
|
897
|
+
const argv: unknown = JSON.parse(command);
|
|
898
|
+
if (Array.isArray(argv)) {
|
|
899
|
+
const name = bunCommand(String(argv[0]));
|
|
900
|
+
return name ? [{ command: name, args: argv.slice(1).map(String) }] : [];
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
// Not a JSON exec form: fall through and read it as shell.
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return command
|
|
907
|
+
.replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "'…'")
|
|
908
|
+
.split(/&&|\|\||[;|&(){}`\n]/)
|
|
909
|
+
.flatMap((segment) => {
|
|
910
|
+
const words = segment.trim().split(/\s+/);
|
|
911
|
+
const start = words.findIndex(
|
|
912
|
+
(word) => !SHELL_COMMAND_PREFIX.has(word) && !/^[A-Za-z_]\w*=/.test(word),
|
|
913
|
+
);
|
|
914
|
+
const name = bunCommand(words[start]);
|
|
915
|
+
return name ? [{ command: name, args: words.slice(start + 1) }] : [];
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
803
919
|
const DOCKERFILE_BUILD_PLATFORM = /--platform=\$\{?BUILDPLATFORM\}?(?:\s|$)/;
|
|
804
|
-
const
|
|
920
|
+
const BUN_INSTALL_SUBCOMMANDS = new Set(['install', 'i', 'add', 'a']);
|
|
921
|
+
const BUNFIG_SOURCE = /(?:^|[\s/"'[,])bunfig\.toml(?=[\s"',\]]|$)/;
|
|
922
|
+
|
|
923
|
+
/** Whether a `COPY`/`ADD` brings `bunfig.toml` into the stage, by name or with the whole context. */
|
|
924
|
+
function copiesBunfig(args: string): boolean {
|
|
925
|
+
if (BUNFIG_SOURCE.test(args)) return true;
|
|
926
|
+
const words = args.split(/\s+/);
|
|
927
|
+
if (words.some((word) => word.startsWith('--from='))) return false;
|
|
928
|
+
const sources = words.filter((word) => !word.startsWith('--')).slice(0, -1);
|
|
929
|
+
return sources.some((source) => source === '.' || source === './');
|
|
930
|
+
}
|
|
805
931
|
|
|
806
932
|
/**
|
|
807
|
-
* Check 15:
|
|
808
|
-
*
|
|
809
|
-
*
|
|
933
|
+
* Check 15: a Dockerfile stage built for the target platform must not run
|
|
934
|
+
* JavaScript while it builds. A multi-arch `docker buildx` build runs every
|
|
935
|
+
* such stage under QEMU on the non-native leg, where bun >= 1.4 aborts, and the
|
|
936
|
+
* image publishes last — after npm and the MCP Registry. A stage counts as
|
|
937
|
+
* running JavaScript when a `RUN` invokes `bun` for anything but
|
|
938
|
+
* `install`/`add` (`bun run build`, `bun -e`, a script, `bunx`), or runs
|
|
939
|
+
* `bun install`/`bun add` once `bunfig.toml` is in the stage, since its
|
|
940
|
+
* `[install.security]` scanner runs as a Bun program. A stage whose `FROM`
|
|
941
|
+
* carries `--platform=$BUILDPLATFORM` is exempt, and so are `HEALTHCHECK`,
|
|
942
|
+
* `CMD`, and `ENTRYPOINT`, which run at container start on the real target.
|
|
943
|
+
* One error per stage, naming each offending line.
|
|
810
944
|
*/
|
|
811
945
|
export function checkDockerfileBuildPlatform(dockerfile: string): string[] {
|
|
812
|
-
const stages: {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
946
|
+
const stages: {
|
|
947
|
+
alias: string | undefined;
|
|
948
|
+
bunfig: boolean;
|
|
949
|
+
findings: string[];
|
|
950
|
+
from: DockerInstruction;
|
|
951
|
+
pinned: boolean;
|
|
952
|
+
}[] = [];
|
|
953
|
+
|
|
954
|
+
for (const instruction of dockerInstructions(dockerfile)) {
|
|
955
|
+
if (instruction.keyword === 'FROM') {
|
|
956
|
+
const [image = '', , alias] = instruction.args
|
|
957
|
+
.toLowerCase()
|
|
958
|
+
.split(/\s+/)
|
|
959
|
+
.filter((word) => !word.startsWith('--'));
|
|
960
|
+
stages.push({
|
|
961
|
+
alias,
|
|
962
|
+
// A stage built FROM an earlier one starts with that stage's files.
|
|
963
|
+
bunfig: stages.find((earlier) => earlier.alias === image)?.bunfig ?? false,
|
|
964
|
+
findings: [],
|
|
965
|
+
from: instruction,
|
|
966
|
+
pinned: DOCKERFILE_BUILD_PLATFORM.test(instruction.args),
|
|
967
|
+
});
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const stage = stages.at(-1);
|
|
971
|
+
if (!stage) continue;
|
|
972
|
+
if (instruction.keyword === 'COPY' || instruction.keyword === 'ADD') {
|
|
973
|
+
stage.bunfig ||= copiesBunfig(instruction.args);
|
|
974
|
+
} else if (instruction.keyword === 'RUN' && !stage.pinned) {
|
|
975
|
+
const finding = bunInvocations(instruction.args)
|
|
976
|
+
.map(({ command, args }) => {
|
|
977
|
+
const subcommand = args.find((arg) => !arg.startsWith('-'));
|
|
978
|
+
if (command === 'bun' && BUN_INSTALL_SUBCOMMANDS.has(subcommand ?? '')) {
|
|
979
|
+
return stage.bunfig
|
|
980
|
+
? `\`bun ${subcommand}\` after bunfig.toml is in the stage, which starts its security scanner as a Bun program`
|
|
981
|
+
: undefined;
|
|
982
|
+
}
|
|
983
|
+
return `\`${[command, args[0]].filter(Boolean).join(' ')}\``;
|
|
984
|
+
})
|
|
985
|
+
.find(Boolean);
|
|
986
|
+
if (finding) stage.findings.push(`Dockerfile:${instruction.line} runs ${finding}`);
|
|
819
987
|
}
|
|
820
988
|
}
|
|
821
989
|
|
|
822
990
|
return stages
|
|
823
|
-
.filter((stage) => stage.
|
|
991
|
+
.filter((stage) => stage.findings.length > 0)
|
|
824
992
|
.map(
|
|
825
|
-
(
|
|
826
|
-
`Dockerfile:${
|
|
827
|
-
`
|
|
828
|
-
`
|
|
993
|
+
({ from, findings }) =>
|
|
994
|
+
`Dockerfile:${from.line} "${from.text}" builds for the target platform but runs JavaScript while ` +
|
|
995
|
+
`building — ${findings.join('; ')}. A multi-arch buildx build runs that under QEMU on the non-native ` +
|
|
996
|
+
`leg, where bun aborts and no image publishes. Move these steps into a stage that starts ` +
|
|
997
|
+
`"FROM --platform=$BUILDPLATFORM" (cross-install with \`bun install --os=<os> --cpu=<x64|arm64>\`) and ` +
|
|
998
|
+
`copy their output (dist/, node_modules/) into this stage`,
|
|
829
999
|
);
|
|
830
1000
|
}
|
|
831
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
|
+
|
|
832
1078
|
/** Read `packaging.pluginManifests` from devcheck.config.json; default on. */
|
|
833
1079
|
function pluginManifestsEnabled(): boolean {
|
|
834
1080
|
const cfg = tryReadJson<{ packaging?: { pluginManifests?: boolean } }>(
|
|
@@ -846,6 +1092,7 @@ async function main(): Promise<void> {
|
|
|
846
1092
|
resolve('package.json'),
|
|
847
1093
|
);
|
|
848
1094
|
const unscopedName = pkg?.name?.split('/').pop();
|
|
1095
|
+
const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
|
|
849
1096
|
|
|
850
1097
|
// ── Manifest-dependent checks (1–4 + manifest identity) ──
|
|
851
1098
|
const manifestPath = resolve('manifest.json');
|
|
@@ -878,7 +1125,6 @@ async function main(): Promise<void> {
|
|
|
878
1125
|
|
|
879
1126
|
errors.push(...checkManifestUserConfigWiring(manifest));
|
|
880
1127
|
|
|
881
|
-
const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
|
|
882
1128
|
if (serverJson) {
|
|
883
1129
|
const manifestEnv = manifest.server?.mcp_config?.env ?? {};
|
|
884
1130
|
const manifestEnvKeys = new Set(Object.keys(manifestEnv));
|
|
@@ -922,6 +1168,11 @@ async function main(): Promise<void> {
|
|
|
922
1168
|
notes.push('No manifest.json — skipping manifest/server.json alignment checks.');
|
|
923
1169
|
}
|
|
924
1170
|
|
|
1171
|
+
// ── server.json npm launch shape (check 16) ──
|
|
1172
|
+
if (serverJson) {
|
|
1173
|
+
errors.push(...checkServerJsonLaunch(serverJson));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
925
1176
|
// ── Bundle-content guard (checks 5–7) ──
|
|
926
1177
|
const mcpbignorePath = resolve('.mcpbignore');
|
|
927
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
|
+
}
|
package/templates/.env.example
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
# ── Auth ──────────────────────────────────────────────────────────────
|
|
10
10
|
# MCP_AUTH_MODE=none # none | jwt | oauth (default: none)
|
|
11
11
|
# MCP_AUTH_SECRET_KEY= # JWT secret (required for jwt mode)
|
|
12
|
+
# MCP_REQUEST_STATE_KEY= # Opt-in, >= 32 bytes, same on every instance: seals the requestState
|
|
13
|
+
# handlers return; any other state is rejected before the handler runs
|
|
12
14
|
|
|
13
15
|
# ── Storage ───────────────────────────────────────────────────────────
|
|
14
16
|
# STORAGE_PROVIDER_TYPE=in-memory # in-memory | filesystem | supabase | cloudflare-r2 | cloudflare-kv | cloudflare-d1
|
|
@@ -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/AGENTS.md
CHANGED
|
@@ -201,11 +201,12 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
201
201
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
202
202
|
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
203
203
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
204
|
-
| `ctx.inputs` |
|
|
204
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
205
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
205
206
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
206
207
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
207
208
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
208
|
-
| `ctx.requestId` |
|
|
209
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
209
210
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
210
211
|
|
|
211
212
|
---
|
|
@@ -214,7 +215,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
214
215
|
|
|
215
216
|
Handlers throw — the framework catches, classifies, and formats.
|
|
216
217
|
|
|
217
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move.
|
|
218
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
218
219
|
|
|
219
220
|
```ts
|
|
220
221
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -226,7 +227,7 @@ errors: [
|
|
|
226
227
|
],
|
|
227
228
|
async handler(input, ctx) {
|
|
228
229
|
const item = await db.find(input.id);
|
|
229
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
230
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
230
231
|
return item;
|
|
231
232
|
}
|
|
232
233
|
```
|
package/templates/CLAUDE.md
CHANGED
|
@@ -201,11 +201,12 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
201
201
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
202
202
|
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
203
203
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
204
|
-
| `ctx.inputs` |
|
|
204
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
205
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
205
206
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
206
207
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
207
208
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
208
|
-
| `ctx.requestId` |
|
|
209
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
209
210
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
210
211
|
|
|
211
212
|
---
|
|
@@ -214,7 +215,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
214
215
|
|
|
215
216
|
Handlers throw — the framework catches, classifies, and formats.
|
|
216
217
|
|
|
217
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move.
|
|
218
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
218
219
|
|
|
219
220
|
```ts
|
|
220
221
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -226,7 +227,7 @@ errors: [
|
|
|
226
227
|
],
|
|
227
228
|
async handler(input, ctx) {
|
|
228
229
|
const item = await db.find(input.id);
|
|
229
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
230
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
230
231
|
return item;
|
|
231
232
|
}
|
|
232
233
|
```
|