@cyanheads/mcp-ts-core 0.13.8 → 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 +51 -24
- package/CLAUDE.md +51 -24
- package/README.md +10 -10
- package/changelog/0.13.x/0.13.10.md +118 -0
- package/changelog/0.13.x/0.13.9.md +113 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +31 -9
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +6 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +21 -5
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +114 -21
- 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/serverManifest.d.ts +6 -0
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +6 -0
- package/dist/core/serverManifest.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 +2 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +37 -3
- 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 +133 -12
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +192 -20
- 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/inputPrevalidation.d.ts +163 -40
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +330 -114
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +40 -19
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +387 -130
- 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/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +65 -9
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.js +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.d.ts +6 -2
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +7 -3
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +82 -18
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +620 -328
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +11 -7
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.js +19 -16
- package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -1
- package/dist/services/mirror/core/defineMirror.d.ts +1 -0
- package/dist/services/mirror/core/defineMirror.d.ts.map +1 -1
- package/dist/services/mirror/core/defineMirror.js +1 -0
- package/dist/services/mirror/core/defineMirror.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/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +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/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/network/pacer.d.ts +38 -5
- package/dist/utils/network/pacer.d.ts.map +1 -1
- package/dist/utils/network/pacer.js +87 -25
- package/dist/utils/network/pacer.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +10 -5
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +10 -5
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +3 -3
- package/framework-skills/add-export/SKILL.md +5 -16
- package/framework-skills/add-prompt/SKILL.md +7 -3
- package/framework-skills/add-resource/SKILL.md +7 -5
- 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 +40 -42
- package/framework-skills/api-auth/SKILL.md +2 -2
- package/framework-skills/api-canvas/SKILL.md +17 -8
- package/framework-skills/api-config/SKILL.md +5 -4
- package/framework-skills/api-context/SKILL.md +168 -42
- package/framework-skills/api-errors/SKILL.md +48 -51
- package/framework-skills/api-linter/SKILL.md +30 -35
- package/framework-skills/api-mirror/SKILL.md +2 -1
- package/framework-skills/api-telemetry/SKILL.md +14 -10
- package/framework-skills/api-testing/SKILL.md +43 -11
- package/framework-skills/api-utils/SKILL.md +2 -2
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/design-mcp-server/SKILL.md +6 -6
- package/framework-skills/field-test/SKILL.md +5 -5
- package/framework-skills/git-wrapup/SKILL.md +8 -6
- package/framework-skills/orchestrations/SKILL.md +7 -6
- package/framework-skills/orchestrations/workflows/field-test-fix.md +9 -19
- package/framework-skills/orchestrations/workflows/fix-wrapup-release.md +7 -7
- package/framework-skills/orchestrations/workflows/greenfield-build.md +8 -5
- package/framework-skills/orchestrations/workflows/maintenance-release.md +8 -8
- package/framework-skills/polish-docs-meta/SKILL.md +4 -4
- package/framework-skills/release-and-publish/SKILL.md +8 -6
- package/framework-skills/release-pr-review/SKILL.md +38 -24
- package/framework-skills/report-issue-framework/SKILL.md +7 -5
- package/framework-skills/report-issue-local/SKILL.md +8 -6
- package/framework-skills/security-pass/SKILL.md +14 -13
- package/package.json +6 -5
- package/scripts/devcheck.ts +7 -6
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-mcp.ts +87 -27
- package/scripts/lint-packaging.ts +226 -4
- package/scripts/release-github.ts +117 -5
- 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/_.mcpbignore +2 -0
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
* The framework itself has no `manifest.json`/`.mcpb`, so the attach path is
|
|
19
19
|
* skipped here but scaffolded servers that do have a manifest get the full flow.
|
|
20
20
|
*
|
|
21
|
+
* Before any `gh` call it validates the tag — `--notes-from-tag` publishes the
|
|
22
|
+
* message verbatim as the release body, so a malformed tag becomes a malformed
|
|
23
|
+
* public release. `--check` runs only that validation, for use right after
|
|
24
|
+
* `git tag -a` and before the tag is pushed. The rules are the ones the
|
|
25
|
+
* `release-and-publish` skill states for the annotation: an annotated tag, a
|
|
26
|
+
* subject of at most 72 characters with no version and no `;`, flat bullets
|
|
27
|
+
* with no section headers, no signature block leaked into the body, and the
|
|
28
|
+
* `[CHANGELOG v<version>](…)` link as the final line.
|
|
29
|
+
*
|
|
21
30
|
* @module scripts/release-github
|
|
22
31
|
*
|
|
23
32
|
* @example
|
|
@@ -25,6 +34,10 @@
|
|
|
25
34
|
* // bun run release:github
|
|
26
35
|
*
|
|
27
36
|
* @example
|
|
37
|
+
* // Validate the tag annotation only (exit 1 on a violation, no gh calls):
|
|
38
|
+
* // bun run release:github -- --check
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
28
41
|
* // Dry-run — print the command that would be executed without running it:
|
|
29
42
|
* // bun run release:github -- --dry-run
|
|
30
43
|
*/
|
|
@@ -33,8 +46,86 @@ import { spawnSync } from 'node:child_process';
|
|
|
33
46
|
import { existsSync, readFileSync } from 'node:fs';
|
|
34
47
|
import { resolve } from 'node:path';
|
|
35
48
|
import process from 'node:process';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
36
50
|
|
|
37
51
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
52
|
+
const CHECK_ONLY = process.argv.includes('--check');
|
|
53
|
+
|
|
54
|
+
/** A tag subject longer than this reads as a digest, not a release title. */
|
|
55
|
+
const MAX_SUBJECT_LENGTH = 72;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Section headers belong in the changelog entry, never in the tag body: a
|
|
59
|
+
* markdown heading, a Keep a Changelog section name, or any other line that is
|
|
60
|
+
* not a bullet and ends in a colon (`Dependency bumps:`, `Highlights:`).
|
|
61
|
+
*/
|
|
62
|
+
const SECTION_HEADER =
|
|
63
|
+
/^(?:#{1,6}\s.*|(?:Added|Changed|Deprecated|Removed|Fixed|Security|Dependencies)\s*:?|[^\s\-*+[].*:)$/;
|
|
64
|
+
|
|
65
|
+
/** The parts of an annotated tag that become the GitHub Release title and body. */
|
|
66
|
+
export interface TagMessage {
|
|
67
|
+
body: string;
|
|
68
|
+
/** `git for-each-ref %(objecttype)` — `tag` for an annotated tag, `commit` for a lightweight one. */
|
|
69
|
+
objectType: string;
|
|
70
|
+
subject: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validates a tag annotation against the release-body rules. Returns one
|
|
75
|
+
* message per violation; an empty array means the tag is publishable.
|
|
76
|
+
*/
|
|
77
|
+
export function checkTagMessage(tag: TagMessage, version: string): string[] {
|
|
78
|
+
if (tag.objectType !== 'tag') {
|
|
79
|
+
return [
|
|
80
|
+
`v${version} is a lightweight tag — recreate it annotated: git tag -a v${version} -F <file>`,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const errors: string[] = [];
|
|
85
|
+
const { subject } = tag;
|
|
86
|
+
if (subject.length > MAX_SUBJECT_LENGTH) {
|
|
87
|
+
errors.push(
|
|
88
|
+
`subject is ${subject.length} characters — keep it one short theme (≤${MAX_SUBJECT_LENGTH}); the bullets carry the digest`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (subject.includes(version)) {
|
|
92
|
+
errors.push(
|
|
93
|
+
`subject contains the version "${version}" — GitHub prepends "v${version}:" to the title`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (subject.includes(';')) {
|
|
97
|
+
errors.push('subject contains ";" — one theme, not a list of changes');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (tag.body.includes('-----BEGIN')) {
|
|
101
|
+
errors.push(
|
|
102
|
+
'body contains a signature block — the signature did not parse (usually --cleanup=verbatim); recreate the tag with --cleanup=whitespace before it is pushed',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lines = tag.body
|
|
107
|
+
.split('\n')
|
|
108
|
+
.map((line) => line.trim())
|
|
109
|
+
.filter((line) => line.length > 0);
|
|
110
|
+
const headers = lines.filter((line) => SECTION_HEADER.test(line));
|
|
111
|
+
if (headers.length > 0) {
|
|
112
|
+
errors.push(
|
|
113
|
+
`body has section headers (${headers.map((h) => `"${h}"`).join(', ')}) — flat bullets only; sections belong in the changelog entry`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
118
|
+
const changelogLink = new RegExp(
|
|
119
|
+
`^\\[CHANGELOG v${escaped}\\]\\(\\S+/changelog/\\d+\\.\\d+\\.x/${escaped}\\.md\\)`,
|
|
120
|
+
);
|
|
121
|
+
if (!changelogLink.test(lines.at(-1) ?? '')) {
|
|
122
|
+
errors.push(
|
|
123
|
+
`final line is not the changelog link — end the body with "[CHANGELOG v${version}](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/${version}.md)"`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
38
129
|
|
|
39
130
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
40
131
|
|
|
@@ -107,15 +198,34 @@ function main(): void {
|
|
|
107
198
|
if (!subject) {
|
|
108
199
|
console.error(
|
|
109
200
|
`Tag ${tag} not found locally or has no subject line. ` +
|
|
110
|
-
`Create the annotated tag first: git tag -a ${tag} -
|
|
201
|
+
`Create the annotated tag first: git tag -a ${tag} -F <file>`,
|
|
111
202
|
);
|
|
112
203
|
process.exit(1);
|
|
113
204
|
}
|
|
114
205
|
|
|
206
|
+
// 3. Validate the annotation — it becomes the public release body verbatim
|
|
207
|
+
const errors = checkTagMessage(
|
|
208
|
+
{
|
|
209
|
+
subject,
|
|
210
|
+
body: run('git', ['for-each-ref', `refs/tags/${tag}`, '--format=%(contents:body)']),
|
|
211
|
+
objectType: run('git', ['for-each-ref', `refs/tags/${tag}`, '--format=%(objecttype)']),
|
|
212
|
+
},
|
|
213
|
+
version,
|
|
214
|
+
);
|
|
215
|
+
if (errors.length > 0) {
|
|
216
|
+
console.error(`Tag ${tag} is not publishable:`);
|
|
217
|
+
for (const error of errors) console.error(` ✗ ${error}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
if (CHECK_ONLY) {
|
|
221
|
+
console.log(`Tag ${tag} OK.`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
115
225
|
const title = `${tag}: ${subject}`;
|
|
116
226
|
const hasMcpb = existsSync('manifest.json');
|
|
117
227
|
|
|
118
|
-
//
|
|
228
|
+
// 4. Build the gh release create command
|
|
119
229
|
const createArgs = [
|
|
120
230
|
'release',
|
|
121
231
|
'create',
|
|
@@ -152,7 +262,7 @@ function main(): void {
|
|
|
152
262
|
console.log(' asset: dist/*.mcpb');
|
|
153
263
|
}
|
|
154
264
|
|
|
155
|
-
//
|
|
265
|
+
// 5. Try to create the release
|
|
156
266
|
const createResult = gh(createArgs, { required: false });
|
|
157
267
|
|
|
158
268
|
if (!createResult.startsWith('__ERROR__:')) {
|
|
@@ -170,7 +280,7 @@ function main(): void {
|
|
|
170
280
|
process.exit(1);
|
|
171
281
|
}
|
|
172
282
|
|
|
173
|
-
//
|
|
283
|
+
// 6. Release already exists — repair: upload asset (if applicable) and set title.
|
|
174
284
|
console.log(`Release ${tag} already exists. Repairing…`);
|
|
175
285
|
|
|
176
286
|
if (hasMcpb) {
|
|
@@ -184,4 +294,6 @@ function main(): void {
|
|
|
184
294
|
console.log(`Release ${tag} repaired.`);
|
|
185
295
|
}
|
|
186
296
|
|
|
187
|
-
|
|
297
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
298
|
+
main();
|
|
299
|
+
}
|
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
|
|
package/templates/_.mcpbignore
CHANGED
|
@@ -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 });
|