@cyanheads/mcp-ts-core 0.13.9 → 0.13.10
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 +36 -13
- package/CLAUDE.md +36 -13
- package/README.md +9 -9
- package/changelog/0.13.x/0.13.10.md +118 -0
- 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 +89 -20
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +40 -0
- 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 +42 -14
- 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/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/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/performance.d.ts +4 -2
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -6
- 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/telemetry/attributes.d.ts +5 -4
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +5 -4
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +6 -3
- package/framework-skills/add-tool/SKILL.md +29 -33
- package/framework-skills/api-config/SKILL.md +2 -1
- package/framework-skills/api-context/SKILL.md +155 -40
- package/framework-skills/api-errors/SKILL.md +43 -47
- package/framework-skills/api-linter/SKILL.md +5 -29
- package/framework-skills/api-telemetry/SKILL.md +11 -7
- package/framework-skills/api-testing/SKILL.md +43 -11
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/design-mcp-server/SKILL.md +5 -5
- package/framework-skills/field-test/SKILL.md +2 -2
- package/framework-skills/polish-docs-meta/SKILL.md +2 -2
- package/framework-skills/release-and-publish/SKILL.md +3 -3
- package/framework-skills/release-pr-review/SKILL.md +2 -2
- package/framework-skills/security-pass/SKILL.md +8 -7
- package/package.json +4 -3
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-packaging.ts +188 -27
- package/templates/.env.example +2 -0
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +67 -50
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
|
@@ -69,12 +69,17 @@
|
|
|
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`.
|
|
78
83
|
*
|
|
79
84
|
* Every check skips cleanly when its input is absent — consumers who deleted
|
|
80
85
|
* `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
|
|
@@ -183,10 +188,10 @@ function tryReadJson<T>(path: string): T | undefined {
|
|
|
183
188
|
* to evaluate which paths survive the ignore rules. Returns an array of error
|
|
184
189
|
* strings; empty means all checks passed.
|
|
185
190
|
*
|
|
186
|
-
* **Context note:**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
191
|
+
* **Context note:** `ignore` is a devDependency of the framework and of every
|
|
192
|
+
* scaffold (`templates/package.json`), so it resolves wherever
|
|
193
|
+
* `bun run lint:packaging` runs against an `.mcpbignore` — a server project, or
|
|
194
|
+
* mcp-ts-core itself. Where it cannot load, the guard is skipped.
|
|
190
195
|
*/
|
|
191
196
|
interface IgnoreMatcher {
|
|
192
197
|
add(patterns: string[]): IgnoreMatcher;
|
|
@@ -799,33 +804,189 @@ export function checkManifestVersion(manifest: Manifest, packageVersion?: string
|
|
|
799
804
|
];
|
|
800
805
|
}
|
|
801
806
|
|
|
802
|
-
|
|
807
|
+
/** One Dockerfile instruction: continuations joined, a `RUN` heredoc body folded in. */
|
|
808
|
+
interface DockerInstruction {
|
|
809
|
+
args: string;
|
|
810
|
+
keyword: string;
|
|
811
|
+
/** 1-based line the instruction starts on. */
|
|
812
|
+
line: number;
|
|
813
|
+
text: string;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const DOCKERFILE_SKIPPED_LINE = /^\s*(?:#|$)/;
|
|
817
|
+
const DOCKERFILE_CONTINUATION = /\\\s*$/;
|
|
818
|
+
const DOCKERFILE_HEREDOC = /<<-?(["']?)([A-Za-z_]\w*)\1/g;
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Splits a Dockerfile into instructions the way BuildKit reads it: a trailing
|
|
822
|
+
* `\` continues onto the next line, comment and blank lines inside a
|
|
823
|
+
* continuation are dropped, and a `RUN` heredoc's body belongs to its `RUN`.
|
|
824
|
+
*/
|
|
825
|
+
function dockerInstructions(dockerfile: string): DockerInstruction[] {
|
|
826
|
+
const lines = dockerfile.split(/\r?\n/);
|
|
827
|
+
const instructions: DockerInstruction[] = [];
|
|
828
|
+
for (let i = 0; i < lines.length; i++) {
|
|
829
|
+
if (DOCKERFILE_SKIPPED_LINE.test(lines[i] ?? '')) continue;
|
|
830
|
+
const line = i + 1;
|
|
831
|
+
let text = (lines[i] ?? '').trim();
|
|
832
|
+
while (DOCKERFILE_CONTINUATION.test(text) && i + 1 < lines.length) {
|
|
833
|
+
text = text.replace(DOCKERFILE_CONTINUATION, ' ');
|
|
834
|
+
do i++;
|
|
835
|
+
while (i < lines.length && DOCKERFILE_SKIPPED_LINE.test(lines[i] ?? ''));
|
|
836
|
+
text += (lines[i] ?? '').trim();
|
|
837
|
+
}
|
|
838
|
+
const keyword = (text.split(/\s/, 1)[0] ?? '').toUpperCase();
|
|
839
|
+
if (keyword === 'RUN') {
|
|
840
|
+
for (const [, , delimiter] of text.matchAll(DOCKERFILE_HEREDOC)) {
|
|
841
|
+
while (i + 1 < lines.length && (lines[++i] ?? '').trim() !== delimiter) {
|
|
842
|
+
text += `\n${lines[i]}`;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
instructions.push({ keyword, args: text.slice(keyword.length).trim(), line, text });
|
|
847
|
+
}
|
|
848
|
+
return instructions;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Words that may precede the command itself in a simple shell command. */
|
|
852
|
+
const SHELL_COMMAND_PREFIX = new Set([
|
|
853
|
+
'!',
|
|
854
|
+
'command',
|
|
855
|
+
'do',
|
|
856
|
+
'elif',
|
|
857
|
+
'else',
|
|
858
|
+
'env',
|
|
859
|
+
'exec',
|
|
860
|
+
'if',
|
|
861
|
+
'then',
|
|
862
|
+
'time',
|
|
863
|
+
'until',
|
|
864
|
+
'while',
|
|
865
|
+
]);
|
|
866
|
+
|
|
867
|
+
/** A `bun`/`bunx` invocation in a `RUN`: the command word and the words after it. */
|
|
868
|
+
interface BunInvocation {
|
|
869
|
+
args: string[];
|
|
870
|
+
command: 'bun' | 'bunx';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function bunCommand(word: string | undefined): BunInvocation['command'] | undefined {
|
|
874
|
+
const name = word?.split('/').pop();
|
|
875
|
+
return name === 'bun' || name === 'bunx' ? name : undefined;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The `bun`/`bunx` invocations a `RUN` instruction's command makes, read from
|
|
880
|
+
* command position only — `chown bun:bun`, `/root/.bun/`, and a quoted
|
|
881
|
+
* mention are not invocations. Quoted strings are opaque, so a `bun -e '…'`
|
|
882
|
+
* program never splits into commands of its own.
|
|
883
|
+
*/
|
|
884
|
+
function bunInvocations(runArgs: string): BunInvocation[] {
|
|
885
|
+
const command = runArgs.replace(/^(?:--[\w-]+=\S+\s+)*/, '');
|
|
886
|
+
if (command.startsWith('[')) {
|
|
887
|
+
try {
|
|
888
|
+
const argv: unknown = JSON.parse(command);
|
|
889
|
+
if (Array.isArray(argv)) {
|
|
890
|
+
const name = bunCommand(String(argv[0]));
|
|
891
|
+
return name ? [{ command: name, args: argv.slice(1).map(String) }] : [];
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
// Not a JSON exec form: fall through and read it as shell.
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return command
|
|
898
|
+
.replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "'…'")
|
|
899
|
+
.split(/&&|\|\||[;|&(){}`\n]/)
|
|
900
|
+
.flatMap((segment) => {
|
|
901
|
+
const words = segment.trim().split(/\s+/);
|
|
902
|
+
const start = words.findIndex(
|
|
903
|
+
(word) => !SHELL_COMMAND_PREFIX.has(word) && !/^[A-Za-z_]\w*=/.test(word),
|
|
904
|
+
);
|
|
905
|
+
const name = bunCommand(words[start]);
|
|
906
|
+
return name ? [{ command: name, args: words.slice(start + 1) }] : [];
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
803
910
|
const DOCKERFILE_BUILD_PLATFORM = /--platform=\$\{?BUILDPLATFORM\}?(?:\s|$)/;
|
|
804
|
-
const
|
|
911
|
+
const BUN_INSTALL_SUBCOMMANDS = new Set(['install', 'i', 'add', 'a']);
|
|
912
|
+
const BUNFIG_SOURCE = /(?:^|[\s/"'[,])bunfig\.toml(?=[\s"',\]]|$)/;
|
|
913
|
+
|
|
914
|
+
/** Whether a `COPY`/`ADD` brings `bunfig.toml` into the stage, by name or with the whole context. */
|
|
915
|
+
function copiesBunfig(args: string): boolean {
|
|
916
|
+
if (BUNFIG_SOURCE.test(args)) return true;
|
|
917
|
+
const words = args.split(/\s+/);
|
|
918
|
+
if (words.some((word) => word.startsWith('--from='))) return false;
|
|
919
|
+
const sources = words.filter((word) => !word.startsWith('--')).slice(0, -1);
|
|
920
|
+
return sources.some((source) => source === '.' || source === './');
|
|
921
|
+
}
|
|
805
922
|
|
|
806
923
|
/**
|
|
807
|
-
* Check 15:
|
|
808
|
-
*
|
|
809
|
-
*
|
|
924
|
+
* Check 15: a Dockerfile stage built for the target platform must not run
|
|
925
|
+
* JavaScript while it builds. A multi-arch `docker buildx` build runs every
|
|
926
|
+
* such stage under QEMU on the non-native leg, where bun >= 1.4 aborts, and the
|
|
927
|
+
* image publishes last — after npm and the MCP Registry. A stage counts as
|
|
928
|
+
* running JavaScript when a `RUN` invokes `bun` for anything but
|
|
929
|
+
* `install`/`add` (`bun run build`, `bun -e`, a script, `bunx`), or runs
|
|
930
|
+
* `bun install`/`bun add` once `bunfig.toml` is in the stage, since its
|
|
931
|
+
* `[install.security]` scanner runs as a Bun program. A stage whose `FROM`
|
|
932
|
+
* carries `--platform=$BUILDPLATFORM` is exempt, and so are `HEALTHCHECK`,
|
|
933
|
+
* `CMD`, and `ENTRYPOINT`, which run at container start on the real target.
|
|
934
|
+
* One error per stage, naming each offending line.
|
|
810
935
|
*/
|
|
811
936
|
export function checkDockerfileBuildPlatform(dockerfile: string): string[] {
|
|
812
|
-
const stages: {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
937
|
+
const stages: {
|
|
938
|
+
alias: string | undefined;
|
|
939
|
+
bunfig: boolean;
|
|
940
|
+
findings: string[];
|
|
941
|
+
from: DockerInstruction;
|
|
942
|
+
pinned: boolean;
|
|
943
|
+
}[] = [];
|
|
944
|
+
|
|
945
|
+
for (const instruction of dockerInstructions(dockerfile)) {
|
|
946
|
+
if (instruction.keyword === 'FROM') {
|
|
947
|
+
const [image = '', , alias] = instruction.args
|
|
948
|
+
.toLowerCase()
|
|
949
|
+
.split(/\s+/)
|
|
950
|
+
.filter((word) => !word.startsWith('--'));
|
|
951
|
+
stages.push({
|
|
952
|
+
alias,
|
|
953
|
+
// A stage built FROM an earlier one starts with that stage's files.
|
|
954
|
+
bunfig: stages.find((earlier) => earlier.alias === image)?.bunfig ?? false,
|
|
955
|
+
findings: [],
|
|
956
|
+
from: instruction,
|
|
957
|
+
pinned: DOCKERFILE_BUILD_PLATFORM.test(instruction.args),
|
|
958
|
+
});
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const stage = stages.at(-1);
|
|
962
|
+
if (!stage) continue;
|
|
963
|
+
if (instruction.keyword === 'COPY' || instruction.keyword === 'ADD') {
|
|
964
|
+
stage.bunfig ||= copiesBunfig(instruction.args);
|
|
965
|
+
} else if (instruction.keyword === 'RUN' && !stage.pinned) {
|
|
966
|
+
const finding = bunInvocations(instruction.args)
|
|
967
|
+
.map(({ command, args }) => {
|
|
968
|
+
const subcommand = args.find((arg) => !arg.startsWith('-'));
|
|
969
|
+
if (command === 'bun' && BUN_INSTALL_SUBCOMMANDS.has(subcommand ?? '')) {
|
|
970
|
+
return stage.bunfig
|
|
971
|
+
? `\`bun ${subcommand}\` after bunfig.toml is in the stage, which starts its security scanner as a Bun program`
|
|
972
|
+
: undefined;
|
|
973
|
+
}
|
|
974
|
+
return `\`${[command, args[0]].filter(Boolean).join(' ')}\``;
|
|
975
|
+
})
|
|
976
|
+
.find(Boolean);
|
|
977
|
+
if (finding) stage.findings.push(`Dockerfile:${instruction.line} runs ${finding}`);
|
|
819
978
|
}
|
|
820
979
|
}
|
|
821
980
|
|
|
822
981
|
return stages
|
|
823
|
-
.filter((stage) => stage.
|
|
982
|
+
.filter((stage) => stage.findings.length > 0)
|
|
824
983
|
.map(
|
|
825
|
-
(
|
|
826
|
-
`Dockerfile:${
|
|
827
|
-
`
|
|
828
|
-
`
|
|
984
|
+
({ from, findings }) =>
|
|
985
|
+
`Dockerfile:${from.line} "${from.text}" builds for the target platform but runs JavaScript while ` +
|
|
986
|
+
`building — ${findings.join('; ')}. A multi-arch buildx build runs that under QEMU on the non-native ` +
|
|
987
|
+
`leg, where bun aborts and no image publishes. Move these steps into a stage that starts ` +
|
|
988
|
+
`"FROM --platform=$BUILDPLATFORM" (cross-install with \`bun install --os=<os> --cpu=<x64|arm64>\`) and ` +
|
|
989
|
+
`copy their output (dist/, node_modules/) into this stage`,
|
|
829
990
|
);
|
|
830
991
|
}
|
|
831
992
|
|
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
|
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
|
```
|
package/templates/Dockerfile
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
#
|
|
7
7
|
# Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
|
|
8
|
-
# JavaScript, and only `dist/` crosses into the production stage
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# JavaScript, and only `dist/` crosses into the production stage. Built for the
|
|
9
|
+
# target instead, the non-native leg of a `--platform linux/amd64,linux/arm64`
|
|
10
|
+
# build runs under QEMU, where bun >= 1.4 aborts with a JavaScriptCore allocator
|
|
11
|
+
# assertion and fails the multi-arch push.
|
|
12
12
|
#
|
|
13
13
|
# The constraint this assumes: the build stage produces platform-independent
|
|
14
14
|
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
@@ -34,80 +34,97 @@ RUN bun run build
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
# ==============================================================================
|
|
37
|
-
# Production Stage
|
|
37
|
+
# Production Dependencies Stage
|
|
38
38
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# Installs the production dependency tree for the target platform. Every step
|
|
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.
|
|
45
|
+
#
|
|
46
|
+
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
47
|
+
# devDependencies.
|
|
42
48
|
# ==============================================================================
|
|
43
|
-
FROM oven/bun:1.4.2
|
|
49
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
|
|
44
50
|
|
|
45
51
|
WORKDIR /usr/src/app
|
|
46
52
|
|
|
47
|
-
# Set the environment to production for performance and to ensure only
|
|
48
|
-
# production dependencies are installed.
|
|
49
|
-
ENV NODE_ENV=production
|
|
50
|
-
|
|
51
|
-
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
52
|
-
ARG APP_VERSION
|
|
53
|
-
LABEL org.opencontainers.image.title="{{PACKAGE_NAME}}"
|
|
54
|
-
LABEL org.opencontainers.image.description=""
|
|
55
|
-
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
56
|
-
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
57
|
-
LABEL org.opencontainers.image.source=""
|
|
58
|
-
|
|
59
53
|
# Copy dependency manifests. `bunfig.toml` rides along so every install below
|
|
60
54
|
# passes its release-age gate and security scanner, as a local install does.
|
|
61
55
|
COPY package.json bun.lock bunfig.toml ./
|
|
62
56
|
|
|
63
57
|
# The scanner bunfig.toml names is a devDependency, and Bun installs a missing
|
|
64
58
|
# scanner through the same production-filtered install, which omits it and
|
|
65
|
-
# aborts. Seed it from the build stage's full install instead. Remove this line
|
|
66
|
-
#
|
|
59
|
+
# aborts. Seed it from the build stage's full install instead. Remove this line,
|
|
60
|
+
# and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
|
|
67
61
|
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
68
62
|
|
|
63
|
+
# Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
|
|
64
|
+
# `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
|
|
65
|
+
# publishes only these two architectures, so any other target fails here.
|
|
66
|
+
ARG TARGETOS
|
|
67
|
+
ARG TARGETARCH
|
|
68
|
+
RUN case "$TARGETARCH" in \
|
|
69
|
+
amd64) echo x64 ;; \
|
|
70
|
+
arm64) echo arm64 ;; \
|
|
71
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
72
|
+
esac > .bun-cpu
|
|
73
|
+
|
|
69
74
|
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
70
75
|
# that are not needed in the final production image.
|
|
71
76
|
# `--omit=peer` drops the framework's optional peer tiers (test runner, service
|
|
72
77
|
# SDKs, parsers) that Bun would otherwise auto-install. Anything this server
|
|
73
78
|
# actually imports belongs in its own `dependencies`, so nothing needed at
|
|
74
|
-
# runtime is lost.
|
|
75
|
-
# install re-resolves the graph and pulls every optional peer back in.
|
|
79
|
+
# runtime is lost.
|
|
76
80
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
77
|
-
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
81
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
82
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
78
83
|
|
|
79
84
|
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
80
85
|
# Installed by default. Omit them for a leaner image at build time
|
|
81
86
|
# with: docker build --build-arg OTEL_ENABLED=false
|
|
82
|
-
#
|
|
83
|
-
# `peerDependencies
|
|
84
|
-
|
|
87
|
+
# The script reads the list and each range from the installed framework's
|
|
88
|
+
# `peerDependencies` and passes the target flags on to its `bun install`.
|
|
89
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
85
90
|
ARG OTEL_ENABLED=true
|
|
86
91
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
87
92
|
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
88
|
-
|
|
89
|
-
const { peerDependencies: peers } = await Bun.file("node_modules/@cyanheads/mcp-ts-core/package.json").json(); \
|
|
90
|
-
const names = process.argv.slice(1); \
|
|
91
|
-
const missing = names.filter((name) => !peers?.[name]); \
|
|
92
|
-
if (missing.length > 0) throw new Error(`no peerDependencies range for ${missing.join(", ")}`); \
|
|
93
|
-
console.log(names.map((name) => `${name}@${peers[name]}`).join(" ")); \
|
|
94
|
-
' \
|
|
95
|
-
@hono/otel \
|
|
96
|
-
@opentelemetry/api-logs \
|
|
97
|
-
@opentelemetry/exporter-logs-otlp-http \
|
|
98
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
99
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
100
|
-
@opentelemetry/instrumentation-http \
|
|
101
|
-
@opentelemetry/instrumentation-pino \
|
|
102
|
-
@opentelemetry/resources \
|
|
103
|
-
@opentelemetry/sdk-logs \
|
|
104
|
-
@opentelemetry/sdk-metrics \
|
|
105
|
-
@opentelemetry/sdk-node \
|
|
106
|
-
@opentelemetry/sdk-trace-node \
|
|
107
|
-
@opentelemetry/semantic-conventions) \
|
|
108
|
-
&& bun add --omit=dev --omit=peer --ignore-scripts $specs; \
|
|
93
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
109
94
|
fi
|
|
110
95
|
|
|
96
|
+
# The seeded scanner served only the installs above; keep it out of the image.
|
|
97
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ==============================================================================
|
|
101
|
+
# Production Stage
|
|
102
|
+
#
|
|
103
|
+
# This stage creates a minimal, optimized, and secure image for running the
|
|
104
|
+
# application. It uses a slim base image and only includes production
|
|
105
|
+
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
106
|
+
# and CMD, which run on the real target at container start.
|
|
107
|
+
# ==============================================================================
|
|
108
|
+
FROM oven/bun:1.4.2-slim AS production
|
|
109
|
+
|
|
110
|
+
WORKDIR /usr/src/app
|
|
111
|
+
|
|
112
|
+
# Set the environment to production for performance.
|
|
113
|
+
ENV NODE_ENV=production
|
|
114
|
+
|
|
115
|
+
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
116
|
+
ARG APP_VERSION
|
|
117
|
+
LABEL org.opencontainers.image.title="{{PACKAGE_NAME}}"
|
|
118
|
+
LABEL org.opencontainers.image.description=""
|
|
119
|
+
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
120
|
+
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
121
|
+
LABEL org.opencontainers.image.source=""
|
|
122
|
+
|
|
123
|
+
# The manifest comes from the build context: the deps stage's copy was rewritten
|
|
124
|
+
# by the OTel install, and the runtime reads only its name, version, and type.
|
|
125
|
+
COPY package.json ./
|
|
126
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
127
|
+
|
|
111
128
|
# Copy the compiled application code from the build stage
|
|
112
129
|
COPY --from=build /usr/src/app/dist ./dist
|
|
113
130
|
|
|
@@ -27,7 +27,8 @@ export const echoTool = tool('template_echo_message', {
|
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
// Declare each domain failure mode the agent should plan around. The framework
|
|
30
|
-
// types `ctx.fail(reason, …)` against the declared union
|
|
30
|
+
// types `ctx.fail(reason, …)` against the declared union and puts the entry's
|
|
31
|
+
// `recovery` on both client surfaces when the throw carries none. Baseline codes
|
|
31
32
|
// (InternalError, ServiceUnavailable, Timeout, ValidationError,
|
|
32
33
|
// SerializationError) bubble freely — only declare domain-specific reasons.
|
|
33
34
|
// Delete this block if no domain-specific failures apply to your tool.
|
|
@@ -42,13 +43,7 @@ export const echoTool = tool('template_echo_message', {
|
|
|
42
43
|
|
|
43
44
|
handler(input, ctx) {
|
|
44
45
|
if (input.message.trim().length === 0) {
|
|
45
|
-
|
|
46
|
-
// it the hint reaches neither structuredContent nor content[].
|
|
47
|
-
throw ctx.fail(
|
|
48
|
-
'empty_message',
|
|
49
|
-
'Message must contain at least one non-whitespace character.',
|
|
50
|
-
{ ...ctx.recoveryFor('empty_message') },
|
|
51
|
-
);
|
|
46
|
+
throw ctx.fail('empty_message', 'Message must contain at least one non-whitespace character.');
|
|
52
47
|
}
|
|
53
48
|
// Reaches both client surfaces with no format() plumbing.
|
|
54
49
|
ctx.enrich({ characterCount: input.message.length });
|