@cyanheads/mcp-ts-core 0.10.7 → 0.10.9
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 +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/changelog/0.10.x/0.10.8.md +19 -0
- package/changelog/0.10.x/0.10.9.md +16 -0
- package/dist/core/app.d.ts +6 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +63 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +56 -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/logs/combined.log +19 -8
- package/dist/logs/error.log +10 -4
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +9 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/services/canvas/core/sqlGate.d.ts +2 -0
- package/dist/services/canvas/core/sqlGate.d.ts.map +1 -1
- package/dist/services/canvas/core/sqlGate.js +2 -0
- package/dist/services/canvas/core/sqlGate.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +39 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/testing/index.d.ts +15 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +22 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +3 -2
- package/scripts/check-dependency-specifiers.ts +190 -0
- package/scripts/devcheck.ts +21 -3
- package/scripts/lint-packaging.ts +152 -0
- package/skills/add-tool/SKILL.md +15 -1
- package/skills/api-canvas/SKILL.md +3 -1
- package/skills/api-config/SKILL.md +2 -2
- package/skills/api-context/SKILL.md +55 -2
- package/skills/api-telemetry/SKILL.md +2 -2
- package/skills/polish-docs-meta/SKILL.md +8 -6
- package/templates/devcheck.config.json +3 -0
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
* `createWorkerHandler()` (src/index.ts, src/worker.ts) and manifest
|
|
33
33
|
* `display_name` must equal the unscoped package name; a partial
|
|
34
34
|
* `name`/`title` pair warns without failing (issue #231).
|
|
35
|
+
* 10. Plugin marketplace manifests (`.claude-plugin/plugin.json`,
|
|
36
|
+
* `.codex-plugin/plugin.json`, `.codex-plugin/mcp.json`): non-empty
|
|
37
|
+
* descriptions, and identity/install correctness — display fields
|
|
38
|
+
* (`name`, server key, `interface.displayName`) carry the unscoped machine
|
|
39
|
+
* name while the `npx -y` install arg carries the full `package.json`
|
|
40
|
+
* name (scoped if scoped). An unscoped install arg for a scoped package
|
|
41
|
+
* is a guaranteed install 404. Gated by `devcheck.config.json`
|
|
42
|
+
* `packaging.pluginManifests` (default on); each manifest is skipped
|
|
43
|
+
* cleanly when absent (issue #240).
|
|
35
44
|
*
|
|
36
45
|
* Every check skips cleanly when its input is absent — consumers who deleted
|
|
37
46
|
* `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
|
|
@@ -374,6 +383,128 @@ export function checkManifestIdentity(manifest: Manifest, unscopedName: string):
|
|
|
374
383
|
return [];
|
|
375
384
|
}
|
|
376
385
|
|
|
386
|
+
/** Parsed plugin marketplace manifests; an absent manifest is `undefined`. */
|
|
387
|
+
export interface PluginManifestInputs {
|
|
388
|
+
claudePlugin?: unknown;
|
|
389
|
+
codexMcp?: unknown;
|
|
390
|
+
codexPlugin?: unknown;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null;
|
|
394
|
+
const isNonEmptyString = (v: unknown): boolean => typeof v === 'string' && v.trim().length > 0;
|
|
395
|
+
|
|
396
|
+
/** The `npx -y <pkg>` install target is the arg after the `-y` flag (args[1]). */
|
|
397
|
+
function installArg(entry: Record<string, unknown>): unknown {
|
|
398
|
+
return Array.isArray(entry.args) ? entry.args[1] : undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Check 10: plugin marketplace manifests. Display fields (`name`, server key,
|
|
403
|
+
* `interface.displayName`) must equal the unscoped machine name; the install
|
|
404
|
+
* arg must equal the full `package.json` name (the real `npx` target). Empty
|
|
405
|
+
* descriptions ship blank marketplace cards. Each manifest is validated only
|
|
406
|
+
* when present, so HTTP-only and non-plugin consumers are unaffected. The
|
|
407
|
+
* caller gates the whole check on `packaging.pluginManifests`.
|
|
408
|
+
*/
|
|
409
|
+
export function checkPluginManifests(
|
|
410
|
+
inputs: PluginManifestInputs,
|
|
411
|
+
unscopedName: string,
|
|
412
|
+
fullName: string,
|
|
413
|
+
): string[] {
|
|
414
|
+
const errors: string[] = [];
|
|
415
|
+
const optOut =
|
|
416
|
+
'(or set "packaging": { "pluginManifests": false } in devcheck.config.json to opt out)';
|
|
417
|
+
|
|
418
|
+
// ── .claude-plugin/plugin.json ──
|
|
419
|
+
const claude = inputs.claudePlugin;
|
|
420
|
+
if (isRecord(claude)) {
|
|
421
|
+
const f = '.claude-plugin/plugin.json';
|
|
422
|
+
if (!isNonEmptyString(claude.description)) {
|
|
423
|
+
errors.push(`${f} "description" is empty — populate it ${optOut}`);
|
|
424
|
+
}
|
|
425
|
+
if (claude.name !== unscopedName) {
|
|
426
|
+
errors.push(
|
|
427
|
+
`${f} "name" is "${String(claude.name)}" — must equal the unscoped package name "${unscopedName}"`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const servers = claude.mcpServers;
|
|
431
|
+
if (isRecord(servers)) {
|
|
432
|
+
if (!(unscopedName in servers)) {
|
|
433
|
+
errors.push(
|
|
434
|
+
`${f} mcpServers has no "${unscopedName}" entry — the server key must be the unscoped package name`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const entry = servers[unscopedName];
|
|
438
|
+
if (isRecord(entry) && installArg(entry) !== fullName) {
|
|
439
|
+
errors.push(
|
|
440
|
+
`${f} mcpServers["${unscopedName}"] install arg is "${String(installArg(entry))}" — ` +
|
|
441
|
+
`must be the full package name "${fullName}" (the npx -y target; an unscoped arg for a scoped package 404s)`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── .codex-plugin/plugin.json ──
|
|
448
|
+
const codex = inputs.codexPlugin;
|
|
449
|
+
if (isRecord(codex)) {
|
|
450
|
+
const f = '.codex-plugin/plugin.json';
|
|
451
|
+
if (!isNonEmptyString(codex.description)) {
|
|
452
|
+
errors.push(`${f} "description" is empty — populate it ${optOut}`);
|
|
453
|
+
}
|
|
454
|
+
if (codex.name !== unscopedName) {
|
|
455
|
+
errors.push(
|
|
456
|
+
`${f} "name" is "${String(codex.name)}" — must equal the unscoped package name "${unscopedName}"`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const iface = codex.interface;
|
|
460
|
+
if (isRecord(iface)) {
|
|
461
|
+
if (iface.displayName !== unscopedName) {
|
|
462
|
+
errors.push(
|
|
463
|
+
`${f} interface.displayName is "${String(iface.displayName)}" — must equal the unscoped package name "${unscopedName}"`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (!isNonEmptyString(iface.shortDescription)) {
|
|
467
|
+
errors.push(`${f} interface.shortDescription is empty — populate it ${optOut}`);
|
|
468
|
+
}
|
|
469
|
+
if (!isNonEmptyString(iface.longDescription)) {
|
|
470
|
+
errors.push(`${f} interface.longDescription is empty — populate it ${optOut}`);
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
errors.push(
|
|
474
|
+
`${f} is missing the "interface" object (displayName / shortDescription / longDescription)`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── .codex-plugin/mcp.json ──
|
|
480
|
+
const codexMcp = inputs.codexMcp;
|
|
481
|
+
if (isRecord(codexMcp)) {
|
|
482
|
+
const f = '.codex-plugin/mcp.json';
|
|
483
|
+
if (!(unscopedName in codexMcp)) {
|
|
484
|
+
errors.push(
|
|
485
|
+
`${f} has no "${unscopedName}" server entry — the server key must be the unscoped package name`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const entry = codexMcp[unscopedName];
|
|
489
|
+
if (isRecord(entry) && installArg(entry) !== fullName) {
|
|
490
|
+
errors.push(
|
|
491
|
+
`${f} "${unscopedName}" install arg is "${String(installArg(entry))}" — ` +
|
|
492
|
+
`must be the full package name "${fullName}" (the npx -y target)`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return errors;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Read `packaging.pluginManifests` from devcheck.config.json; default on. */
|
|
501
|
+
function pluginManifestsEnabled(): boolean {
|
|
502
|
+
const cfg = tryReadJson<{ packaging?: { pluginManifests?: boolean } }>(
|
|
503
|
+
resolve('devcheck.config.json'),
|
|
504
|
+
);
|
|
505
|
+
return cfg?.packaging?.pluginManifests ?? true;
|
|
506
|
+
}
|
|
507
|
+
|
|
377
508
|
async function main(): Promise<void> {
|
|
378
509
|
const errors: string[] = [];
|
|
379
510
|
const warnings: string[] = [];
|
|
@@ -496,6 +627,27 @@ async function main(): Promise<void> {
|
|
|
496
627
|
}
|
|
497
628
|
}
|
|
498
629
|
|
|
630
|
+
// ── Plugin marketplace manifests (check 10) ──
|
|
631
|
+
if (unscopedName && pkg?.name) {
|
|
632
|
+
if (pluginManifestsEnabled()) {
|
|
633
|
+
errors.push(
|
|
634
|
+
...checkPluginManifests(
|
|
635
|
+
{
|
|
636
|
+
claudePlugin: tryReadJson(resolve('.claude-plugin/plugin.json')),
|
|
637
|
+
codexPlugin: tryReadJson(resolve('.codex-plugin/plugin.json')),
|
|
638
|
+
codexMcp: tryReadJson(resolve('.codex-plugin/mcp.json')),
|
|
639
|
+
},
|
|
640
|
+
unscopedName,
|
|
641
|
+
pkg.name,
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
} else {
|
|
645
|
+
notes.push(
|
|
646
|
+
'Plugin-manifest checks disabled via devcheck.config.json packaging.pluginManifests.',
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
499
651
|
for (const note of notes) console.log(note);
|
|
500
652
|
for (const warning of warnings) console.warn(` ⚠ ${warning}`);
|
|
501
653
|
|
package/skills/add-tool/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Scaffold a new MCP tool definition. Use when the user asks to add a tool, create a new tool, or implement a new capability for the server.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.15"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -252,6 +252,20 @@ enrichmentTrailer: {
|
|
|
252
252
|
|
|
253
253
|
`structuredContent` always keeps the full structured value; `enrichmentTrailer` only controls the human-facing `content[]` line.
|
|
254
254
|
|
|
255
|
+
### Image / audio output belongs in `ctx.content`
|
|
256
|
+
|
|
257
|
+
When a tool produces image or audio bytes for the calling model to *see or hear* — a rendered chart, a generated frame, synthesized speech — emit them via `ctx.content`, not an `output` field. `ctx.content.image(data, mimeType)` / `.audio(data, mimeType)` prepend a content block to `content[]` after `format()` runs and **never** write to `structuredContent`, so the base64 is carried once instead of duplicating into the typed output. Like `ctx.enrich`, it lives on the base `Context` and is callable from the service layer.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
async handler(input, ctx) {
|
|
261
|
+
const png = await render(input.spec); // base64 PNG
|
|
262
|
+
ctx.content.image(png, 'image/png'); // → content[] block, not structuredContent
|
|
263
|
+
return { width: input.spec.w, height: input.spec.h }; // typed result stays small
|
|
264
|
+
},
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The alternative — declaring `previewData: z.string()` in `output` and emitting the block from `format()` — ships the bytes twice (once in `structuredContent`, once in the block). Reserve `output` for data the agent reasons over; route raw media through `ctx.content`. Test with `getContentBlocks(ctx)`. Full reference: `skills/api-context` § `ctx.content`.
|
|
268
|
+
|
|
255
269
|
### Capped lists must disclose truncation
|
|
256
270
|
|
|
257
271
|
When a tool accepts a cap-like input (`limit`, `per_page`, `page_size`, `max_results`, `max_items`) and returns an array, disclose when the cap was hit — the agent otherwise treats a partial set as complete.
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
DataCanvas primitive reference — a Tier 3 SQL/analytical workspace for tabular MCP servers, backed by DuckDB. Use when registering tables from upstream APIs, running ad-hoc SQL across them, and exporting results. Covers the acquire → register → query → export flow, per-table TTL, the token-sharing pattern for multi-agent collaboration, env config, and Cloudflare Workers fail-closed behavior.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.7"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -150,6 +150,8 @@ Run SQL across registered tables. Returns at most `rowLimit` rows (default 10 00
|
|
|
150
150
|
|
|
151
151
|
Querying a table that does not exist throws `NotFound` (`data.reason: 'missing_table'`) with a recovery hint to re-stage the table or call `describe()`. This happens when a table has expired (per-table TTL), been dropped, or the name is mistyped. The error is `NotFound`, not `ValidationError` — agents should re-stage, not fix the SQL shape.
|
|
152
152
|
|
|
153
|
+
A `SELECT` that parses but fails to prepare for any other reason — a mistyped column, an unknown function, an invalid expression — throws `ValidationError` (`data.reason: 'invalid_sql'`) and preserves the DuckDB binder detail in `data.binderMessage` (e.g. `Referenced column "x" not found...`, often with a candidate suggestion). This is distinct from `non_select_statement`, reserved for statements that genuinely aren't `SELECT`s — here the shape is fine, so the agent should fix the named column or function.
|
|
154
|
+
|
|
153
155
|
```ts
|
|
154
156
|
const result = await instance.query(`
|
|
155
157
|
SELECT germplasmName, COUNT(*) AS n
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Reference for core and server configuration in `@cyanheads/mcp-ts-core`. Covers env var tables with defaults, priority order, server-specific Zod schema pattern, and Workers lazy-parsing requirement.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.8"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -167,7 +167,7 @@ Activated when both `SUPABASE_URL` and `SUPABASE_ANON_KEY` are set.
|
|
|
167
167
|
| Env Var | `AppConfig` field | Default | Notes |
|
|
168
168
|
|:--------|:-----------------|:--------|:------|
|
|
169
169
|
| `OTEL_ENABLED` | `openTelemetry.enabled` | `false` | Enable OpenTelemetry export |
|
|
170
|
-
| `OTEL_SERVICE_NAME` | `openTelemetry.serviceName` | `package.json` `name` | |
|
|
170
|
+
| `OTEL_SERVICE_NAME` | `openTelemetry.serviceName` | `createApp` `name` → `package.json` `name` | Seeded from `createApp({ name })` when unset; an env value wins |
|
|
171
171
|
| `OTEL_SERVICE_VERSION` | `openTelemetry.serviceVersion` | `package.json` `version` | |
|
|
172
172
|
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `openTelemetry.tracesEndpoint` | — | OTLP traces endpoint URL |
|
|
173
173
|
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `openTelemetry.metricsEndpoint` | — | OTLP metrics endpoint URL |
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api-context
|
|
3
3
|
description: >
|
|
4
|
-
Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.elicit`, `ctx.progress`, `ctx.enrich`), and when to use each.
|
|
4
|
+
Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.elicit`, `ctx.progress`, `ctx.enrich`, `ctx.content`), and when to use each.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.9"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -63,6 +63,12 @@ interface Context {
|
|
|
63
63
|
// declared fields. Kind-tagged helpers: enrich.notice / .total / .echo.
|
|
64
64
|
readonly enrich: Enrich;
|
|
65
65
|
|
|
66
|
+
// Non-text content blocks (image/audio bytes) for the calling model — prepended
|
|
67
|
+
// to content[] after format() runs, never placed in structuredContent. Always
|
|
68
|
+
// present (no-op when never called). Helpers: content.image / .audio; content(block)
|
|
69
|
+
// pushes a raw ContentBlock.
|
|
70
|
+
readonly content: ContentCollect;
|
|
71
|
+
|
|
66
72
|
// Opt-in contract resolver — always present (returns {} when no contract is attached
|
|
67
73
|
// or the reason is unknown), strictly typed on HandlerContext<R> against declared reasons.
|
|
68
74
|
recoveryFor(reason: string): { recovery: { hint: string } } | {};
|
|
@@ -637,6 +643,52 @@ See `add-tool`'s **Tool Response Design** and `skills/api-linter` (`enrichment-*
|
|
|
637
643
|
|
|
638
644
|
---
|
|
639
645
|
|
|
646
|
+
## `ctx.content`
|
|
647
|
+
|
|
648
|
+
Always present on `Context`. Collects **non-text content blocks** — image or audio bytes the calling model should see or hear — and prepends them to the tool's `content[]` after `format()` runs. Collected blocks **never** enter `structuredContent`, so the base64 payload is carried once (in `content[]`) instead of duplicating into the typed output field. The media counterpart to `ctx.enrich`: both ride alongside the domain result without bloating it.
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
export const renderChart = tool('render_chart', {
|
|
652
|
+
description: 'Render a chart from a series and return its summary.',
|
|
653
|
+
input: z.object({ series: z.array(z.number()).describe('Data points') }),
|
|
654
|
+
output: z.object({ points: z.number().describe('Number of points plotted') }),
|
|
655
|
+
async handler(input, ctx) {
|
|
656
|
+
const png = await draw(input.series); // base64 PNG
|
|
657
|
+
ctx.content.image(png, 'image/png'); // → content[] block, NOT structuredContent
|
|
658
|
+
return { points: input.series.length }; // the typed result stays small
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Without `ctx.content`, the only way to surface bytes to the model is to declare them in `output` and emit an image block from `format()` — which ships the base64 twice (once in `structuredContent`, once in the block). `ctx.content` removes the duplication.
|
|
664
|
+
|
|
665
|
+
### Signature
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
// Callable — push a raw ContentBlock (escape hatch for embedded resources, resource links):
|
|
669
|
+
ctx.content(block: ContentBlock): void
|
|
670
|
+
|
|
671
|
+
// Typed helpers for the two base64 media blocks:
|
|
672
|
+
ctx.content.image(data: string, mimeType: string): void // → { type: 'image', data, mimeType }
|
|
673
|
+
ctx.content.audio(data: string, mimeType: string): void // → { type: 'audio', data, mimeType }
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Behavior
|
|
677
|
+
|
|
678
|
+
| Aspect | Detail |
|
|
679
|
+
|:-------|:-------|
|
|
680
|
+
| `content[]` only | Blocks are prepended to `content[]` and never written to `structuredContent`. Data meant for the typed result stays on the handler's return value. |
|
|
681
|
+
| Order | `content[]` is `[...collected blocks, ...format()/JSON output, ...enrichment trailer]` — media first, domain content next, enrichment trailer last. |
|
|
682
|
+
| Accumulation | Each call appends; blocks render in call order. |
|
|
683
|
+
| No-op | A handler that never calls `ctx.content` produces a `content[]` / `structuredContent` byte-identical to before — the feature is purely additive and opt-in. |
|
|
684
|
+
| Error path | If the handler throws, collected blocks are dropped — a failed call returns the error result only, never a partial image. |
|
|
685
|
+
| Service usage | Services accepting `ctx: Context` can call `ctx.content(...)`; the blocks reach `content[]` exactly as if the handler had. |
|
|
686
|
+
| No schema involvement | Blocks bypass `output` entirely, so no linter rule requires them in `format()` and they never appear in the advertised `outputSchema`. |
|
|
687
|
+
|
|
688
|
+
Test content blocks with `getContentBlocks(ctx)` from `@cyanheads/mcp-ts-core/testing`.
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
640
692
|
## Quick reference
|
|
641
693
|
|
|
642
694
|
| Property | Type | Present when |
|
|
@@ -652,6 +704,7 @@ See `add-tool`'s **Tool Response Design** and `skills/api-linter` (`enrichment-*
|
|
|
652
704
|
| `ctx.state` | `ContextState` | Always (throws if `tenantId` missing) |
|
|
653
705
|
| `ctx.signal` | `AbortSignal` | Always |
|
|
654
706
|
| `ctx.enrich` | `Enrich` | Always; typed on `HandlerContext<R, E>` when an `enrichment` block is declared |
|
|
707
|
+
| `ctx.content` | `ContentCollect` | Always — prepends image/audio blocks to `content[]`, never `structuredContent` |
|
|
655
708
|
| `ctx.elicit` | `function \| undefined` | Client supports elicitation |
|
|
656
709
|
| `ctx.notifyResourceListChanged` | `function \| undefined` | Always in handler ctx; delivery request-scoped (see [§ list-changed notifications](#list-changed-notifications-ctxnotify)) |
|
|
657
710
|
| `ctx.notifyResourceUpdated` | `function \| undefined` | Always in handler ctx; delivery request-scoped |
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Catalog of OpenTelemetry instrumentation built into framework `@cyanheads/mcp-ts-core` — spans, metrics, completion logs, env config, runtime caveats, custom instrumentation patterns, and cardinality rules. Use when enabling OTel export, adding custom spans or metrics in services, debugging missing telemetry, looking up attribute names, or deciding what's safe to put on a metric attribute vs. a span.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.2"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -28,7 +28,7 @@ OTel is **off by default**. `OTEL_ENABLED=true` alone does nothing — you also
|
|
|
28
28
|
| `OTEL_ENABLED` | `false` | Master switch. Must be `true` to start the SDK. |
|
|
29
29
|
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | — | OTLP/HTTP traces endpoint (e.g. `http://localhost:4318/v1/traces`). |
|
|
30
30
|
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | — | OTLP/HTTP metrics endpoint (e.g. `http://localhost:4318/v1/metrics`). |
|
|
31
|
-
| `OTEL_SERVICE_NAME` | `package.json` `name` | `service.name` resource attribute. |
|
|
31
|
+
| `OTEL_SERVICE_NAME` | `createApp` `name` → `package.json` `name` | `service.name` resource attribute. Seeded from `createApp({ name })` when unset; an env value wins. |
|
|
32
32
|
| `OTEL_SERVICE_VERSION` | `package.json` `version` | `service.version` resource attribute. |
|
|
33
33
|
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Trace sampling ratio (0–1) for `TraceIdRatioBasedSampler`. |
|
|
34
34
|
| `OTEL_LOG_LEVEL` | `INFO` | OTel diagnostic logger level (`NONE`/`ERROR`/`WARN`/`INFO`/`DEBUG`/`VERBOSE`/`ALL`). |
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Finalize documentation and project metadata for a ship-ready MCP server. Use after implementation is complete, tests pass, and devcheck is clean. Safe to run at any stage — each step checks current state and only acts on what still needs work.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.9"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -76,7 +76,7 @@ Key fields: `name`, `description`, `repository`, `author`, `homepage`, `bugs`, `
|
|
|
76
76
|
|
|
77
77
|
**`name` must communicate the server's domain at a glance.** See `references/package-meta.md` for the naming convention — ambiguous abbreviations and acronym-only names fail the scannability test for humans and agents alike.
|
|
78
78
|
|
|
79
|
-
**`name` and `title` in `createApp()` / `createWorkerHandler()` must match the unscoped `package.json` `name`** — display identity is the machine name on every surface; `lint:packaging` (run by `devcheck`) enforces the match and warns when the pair is partial. `description` is never duplicated into the entrypoint — `package.json` is the canonical source (the framework derives the served description from it).
|
|
79
|
+
**`name` and `title` in `createApp()` / `createWorkerHandler()` must match the unscoped `package.json` `name`** — display identity is the machine name on every surface; `lint:packaging` (run by `devcheck`) enforces the match and warns when the pair is partial. `description` is never duplicated into the entrypoint — `package.json` is the canonical source (the framework derives the served description from it). Adopting the pair also seeds `OTEL_SERVICE_NAME` when unset, so telemetry's `service.name` switches to the machine name on first boot — expect a one-time series split in backends keyed on the old scoped label.
|
|
80
80
|
|
|
81
81
|
**`description` is the canonical source.** Every other surface (README header, `server.json`, Dockerfile OCI label, GitHub repo description) derives from it. Write it here first, then propagate.
|
|
82
82
|
|
|
@@ -169,20 +169,22 @@ Never hand-edit `CHANGELOG.md` when using this pattern — it's a build artifact
|
|
|
169
169
|
|
|
170
170
|
### 10. Plugin Metadata (Codex / Claude Code)
|
|
171
171
|
|
|
172
|
+
`lint:packaging` (run by `devcheck`) now enforces the high-value subset automatically when these manifests are present: non-empty descriptions, and identity/install correctness — display fields (`name`, server key, `interface.displayName`) must be the **unscoped** machine name, while the `npx -y` install arg must be the full `package.json` `name` (scoped if scoped). Opt out per project with `"packaging": { "pluginManifests": false }` in `devcheck.config.json`. The checks below cover the fields the gate doesn't (version / repository / license sync, category, env vars).
|
|
173
|
+
|
|
172
174
|
If `.codex-plugin/plugin.json` exists, verify it's populated and in sync with `package.json` and `server.json`:
|
|
173
175
|
|
|
174
|
-
- `name`
|
|
176
|
+
- `name` is the unscoped `package.json` `name` (display identity is the machine name on every surface)
|
|
175
177
|
- `version` matches `package.json` `version`
|
|
176
178
|
- `description` matches `package.json` `description`
|
|
177
179
|
- `repository` matches `package.json` `repository.url`
|
|
178
180
|
- `license` matches `package.json` `license`
|
|
179
|
-
- `interface.displayName`
|
|
181
|
+
- `interface.displayName` is the unscoped `package.json` `name`
|
|
180
182
|
- `interface.shortDescription` matches `package.json` `description`
|
|
181
183
|
- `interface.category` is set to a meaningful category
|
|
182
184
|
|
|
183
|
-
If `.codex-plugin/mcp.json` exists, verify the server
|
|
185
|
+
If `.codex-plugin/mcp.json` exists, verify the server-name key is the unscoped `package.json` `name`, the `npx -y` install arg is the full `package.json` `name`, and env vars include any required API keys from the server config schema.
|
|
184
186
|
|
|
185
|
-
If `.claude-plugin/plugin.json` exists, apply the same checks: `name
|
|
187
|
+
If `.claude-plugin/plugin.json` exists, apply the same checks: `name` (unscoped), `version`, `description`, `repository`, `license` from `package.json`. Verify the inline `mcpServers` entry key is the unscoped name, its `npx -y` install arg is the full `package.json` `name`, and env vars include any required API keys.
|
|
186
188
|
|
|
187
189
|
### 11. MCPB Bundling Artifacts
|
|
188
190
|
|