@cyanheads/mcp-ts-core 0.8.0 → 0.8.2
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/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/changelog/0.8.x/0.8.0.md +17 -15
- package/changelog/0.8.x/0.8.1.md +17 -0
- package/changelog/0.8.x/0.8.2.md +18 -0
- package/changelog/template.md +13 -0
- package/dist/logs/combined.log +4 -4
- package/dist/logs/error.log +4 -4
- package/dist/mcp-server/transports/http/landing-page/assets/copy-script.d.ts +13 -4
- package/dist/mcp-server/transports/http/landing-page/assets/copy-script.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/assets/copy-script.js +99 -25
- package/dist/mcp-server/transports/http/landing-page/assets/copy-script.js.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/assets/styles.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/assets/styles.js +318 -8
- package/dist/mcp-server/transports/http/landing-page/assets/styles.js.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/sections/status-strip.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/sections/status-strip.js +20 -1
- package/dist/mcp-server/transports/http/landing-page/sections/status-strip.js.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/sections/tools.d.ts +6 -5
- package/dist/mcp-server/transports/http/landing-page/sections/tools.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/sections/tools.js +114 -69
- package/dist/mcp-server/transports/http/landing-page/sections/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/add-app-tool/SKILL.md +24 -8
- package/skills/add-service/SKILL.md +7 -1
- package/skills/add-tool/SKILL.md +3 -1
- package/skills/api-errors/SKILL.md +21 -1
- package/skills/field-test/SKILL.md +73 -13
- package/skills/maintenance/SKILL.md +22 -7
- package/templates/changelog/template.md +18 -5
|
@@ -1,40 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Tools section — responsive
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* @fileoverview Tools section — responsive card grid grouped by safety
|
|
3
|
+
* mutability (read / write / destructive). Each card carries annotation
|
|
4
|
+
* pills, a scope chip, a JSON-RPC invocation snippet, and a collapsible
|
|
5
|
+
* input-schema preview. A filter bar above the grid wires chip + search
|
|
6
|
+
* filtering through `data-mutability` / `data-name` attributes consumed
|
|
7
|
+
* by the inline filter script.
|
|
7
8
|
*
|
|
8
9
|
* @module src/mcp-server/transports/http/landing-page/sections/tools
|
|
9
10
|
*/
|
|
10
11
|
import { html } from '../../../../../utils/formatting/html.js';
|
|
11
12
|
import { renderPill, renderSectionHeading, renderSnippet } from '../primitives.js';
|
|
13
|
+
/**
|
|
14
|
+
* Mutability bucket order — safe defaults first, deliberate engagement last.
|
|
15
|
+
* Filter chips render in this order too.
|
|
16
|
+
*/
|
|
17
|
+
const MUTABILITY_ORDER = ['read', 'write', 'destructive'];
|
|
12
18
|
export function renderToolsSection(tools) {
|
|
13
19
|
if (tools.length === 0)
|
|
14
20
|
return html ``;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const buckets = bucketByMutability(tools);
|
|
22
|
+
const populatedBuckets = MUTABILITY_ORDER.filter((m) => buckets[m].length > 0);
|
|
23
|
+
// A single bucket is redundant with the section header — skip per-group
|
|
24
|
+
// labels in that case but keep `data-mutability` on cards so the filter
|
|
25
|
+
// chips still work.
|
|
26
|
+
const showHeadings = populatedBuckets.length > 1;
|
|
27
|
+
const groups = populatedBuckets.map((mutability) => {
|
|
28
|
+
const bucketTools = buckets[mutability];
|
|
29
|
+
const heading = showHeadings
|
|
30
|
+
? html `<h4 class="group-heading" data-group="${mutability}">${mutability} <span class="group-count">${String(bucketTools.length)}</span></h4>`
|
|
31
|
+
: html ``;
|
|
32
|
+
return html `${heading}<div class="card-grid" data-grid="${mutability}">${bucketTools.map((t) => renderToolCard(t, mutability))}</div>`;
|
|
22
33
|
});
|
|
23
34
|
return html `
|
|
24
|
-
<section aria-labelledby="section-tools">
|
|
35
|
+
<section aria-labelledby="section-tools" data-tools-section>
|
|
25
36
|
${renderSectionHeading('section-tools', 'Tools', tools.length)}
|
|
26
|
-
${
|
|
37
|
+
${renderToolFilterBar(populatedBuckets)}
|
|
38
|
+
<div class="tools-body">${groups}</div>
|
|
39
|
+
<p class="tools-empty" hidden>No tools match the current filter.</p>
|
|
27
40
|
</section>
|
|
28
41
|
`;
|
|
29
42
|
}
|
|
30
|
-
function
|
|
43
|
+
function renderToolFilterBar(populatedBuckets) {
|
|
44
|
+
const chips = [
|
|
45
|
+
html `<button type="button" class="tool-chip" data-filter-mutability="all" aria-pressed="true">all</button>`,
|
|
46
|
+
];
|
|
47
|
+
for (const m of populatedBuckets) {
|
|
48
|
+
chips.push(html `<button type="button" class="tool-chip tool-chip--${m}" data-filter-mutability="${m}" aria-pressed="false">${m}</button>`);
|
|
49
|
+
}
|
|
50
|
+
return html `
|
|
51
|
+
<div class="tool-filter-bar" role="search" aria-label="Filter tools">
|
|
52
|
+
<div class="tool-chips" role="group" aria-label="Filter by mutability">${chips}</div>
|
|
53
|
+
<label class="tool-search">
|
|
54
|
+
<span class="visually-hidden">Search tools</span>
|
|
55
|
+
<input
|
|
56
|
+
type="search"
|
|
57
|
+
data-tool-search
|
|
58
|
+
placeholder="Search tools…"
|
|
59
|
+
autocomplete="off"
|
|
60
|
+
spellcheck="false"
|
|
61
|
+
/>
|
|
62
|
+
</label>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
function renderToolCard(tool, mutability) {
|
|
31
67
|
const anchor = `tool-${tool.name}`;
|
|
32
68
|
const annotations = tool.annotations;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pills.push(renderPill('read-only', 'readonly'));
|
|
36
|
-
if (annotations?.destructiveHint === true)
|
|
37
|
-
pills.push(renderPill('destructive', 'destructive'));
|
|
69
|
+
// Mutability badge first — the safety signal readers track at a glance.
|
|
70
|
+
const pills = [renderPill(mutability, mutability)];
|
|
38
71
|
if (annotations?.openWorldHint)
|
|
39
72
|
pills.push(renderPill('open-world', 'openworld'));
|
|
40
73
|
if (tool.isTask)
|
|
@@ -42,75 +75,87 @@ function renderToolCard(tool) {
|
|
|
42
75
|
if (tool.isApp)
|
|
43
76
|
pills.push(renderPill('app', 'app'));
|
|
44
77
|
const source = tool.sourceUrl
|
|
45
|
-
? html `<a class="source-link" href="${tool.sourceUrl}" rel="noopener">view source ↗</a>`
|
|
78
|
+
? html `<a class="source-link" href="${tool.sourceUrl}" rel="noopener" aria-label="View source for ${tool.name}">view source ↗</a>`
|
|
79
|
+
: html ``;
|
|
80
|
+
const scopeChips = tool.auth && tool.auth.length > 0
|
|
81
|
+
? html `<span class="card-scope" title="${tool.auth.join(', ')}"><span class="card-meta-label">scope</span>${tool.auth.map((scope) => html ` <code class="scope-chip">${scopeAccessLevel(scope)}</code>`)}</span>`
|
|
46
82
|
: html ``;
|
|
47
83
|
const schemaPreview = tool.inputSchema
|
|
48
84
|
? html `
|
|
49
|
-
<details>
|
|
50
|
-
<summary>
|
|
85
|
+
<details class="card-detail">
|
|
86
|
+
<summary>schema</summary>
|
|
51
87
|
<pre><code>${JSON.stringify(tool.inputSchema, null, 2)}</code></pre>
|
|
52
88
|
</details>
|
|
53
89
|
`
|
|
54
90
|
: html ``;
|
|
55
91
|
const invocation = html `
|
|
56
|
-
<details>
|
|
57
|
-
<summary>
|
|
92
|
+
<details class="card-detail">
|
|
93
|
+
<summary>invocation</summary>
|
|
58
94
|
${renderSnippet(`tool-${tool.name}`, buildInvocationSnippet(tool))}
|
|
59
95
|
</details>
|
|
60
96
|
`;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
97
|
+
// Search target: name + description as a single lowercase string. Hidden
|
|
98
|
+
// attribute (not visible) so the filter script can match without parsing
|
|
99
|
+
// DOM text repeatedly. Description gets normalized whitespace so multi-line
|
|
100
|
+
// entries don't waste haystack length.
|
|
101
|
+
const searchTarget = `${tool.name} ${tool.description}`.replace(/\s+/g, ' ').toLowerCase();
|
|
64
102
|
return html `
|
|
65
|
-
<article
|
|
66
|
-
|
|
103
|
+
<article
|
|
104
|
+
class="card tool-card"
|
|
105
|
+
id="${anchor}"
|
|
106
|
+
data-tool-card
|
|
107
|
+
data-mutability="${mutability}"
|
|
108
|
+
data-name="${tool.name}"
|
|
109
|
+
data-search="${searchTarget}"
|
|
110
|
+
>
|
|
111
|
+
<header class="card-head">
|
|
67
112
|
<h3 class="card-title"><a href="#${anchor}">${tool.name}</a></h3>
|
|
68
113
|
<div class="pill-row" role="list">${pills}</div>
|
|
69
114
|
${source}
|
|
70
|
-
</
|
|
115
|
+
</header>
|
|
71
116
|
<p class="card-desc">${tool.description}</p>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
117
|
+
<footer class="card-foot">
|
|
118
|
+
${scopeChips}
|
|
119
|
+
<div class="card-actions">
|
|
120
|
+
${invocation}
|
|
121
|
+
${schemaPreview}
|
|
122
|
+
</div>
|
|
123
|
+
</footer>
|
|
75
124
|
</article>
|
|
76
125
|
`;
|
|
77
126
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
groups.set(prefix, list);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
other.push(tool);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const out = [];
|
|
105
|
-
for (const [prefix, list] of groups) {
|
|
106
|
-
out.push({ label: titleCase(prefix), tools: list });
|
|
107
|
-
}
|
|
108
|
-
if (other.length > 0)
|
|
109
|
-
out.push({ label: 'Other', tools: other });
|
|
110
|
-
return out;
|
|
127
|
+
/**
|
|
128
|
+
* Map a tool to a mutability bucket using its annotations. The MCP spec
|
|
129
|
+
* defaults `destructiveHint` to `true`, but treating annotation-less tools
|
|
130
|
+
* as "destructive" surprises readers — bucket as `write` unless the
|
|
131
|
+
* destructive hint is explicitly set. Mirrors how the annotation pills
|
|
132
|
+
* render today (`pill-destructive` requires `=== true`).
|
|
133
|
+
*/
|
|
134
|
+
function classifyMutability(tool) {
|
|
135
|
+
const a = tool.annotations;
|
|
136
|
+
if (a?.readOnlyHint === true)
|
|
137
|
+
return 'read';
|
|
138
|
+
if (a?.destructiveHint === true)
|
|
139
|
+
return 'destructive';
|
|
140
|
+
return 'write';
|
|
141
|
+
}
|
|
142
|
+
function bucketByMutability(tools) {
|
|
143
|
+
const buckets = { read: [], write: [], destructive: [] };
|
|
144
|
+
for (const tool of tools)
|
|
145
|
+
buckets[classifyMutability(tool)].push(tool);
|
|
146
|
+
return buckets;
|
|
111
147
|
}
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Reduce a colon-delimited scope (`tool:foo:read`) to its trailing access
|
|
150
|
+
* level (`read`). Scopes that don't match the convention render verbatim —
|
|
151
|
+
* the linter doesn't enforce shape, so falling back is friendlier than
|
|
152
|
+
* eating the value.
|
|
153
|
+
*/
|
|
154
|
+
function scopeAccessLevel(scope) {
|
|
155
|
+
const idx = scope.lastIndexOf(':');
|
|
156
|
+
if (idx < 0 || idx === scope.length - 1)
|
|
157
|
+
return scope;
|
|
158
|
+
return scope.slice(idx + 1);
|
|
114
159
|
}
|
|
115
160
|
function buildInvocationSnippet(tool) {
|
|
116
161
|
const args = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../../../../../src/mcp-server/transports/http/landing-page/sections/tools.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../../../../../src/mcp-server/transports/http/landing-page/sections/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,IAAI,EAAiB,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAInF;;;GAGG;AACH,MAAM,gBAAgB,GAA0B,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;AAEjF,MAAM,UAAU,kBAAkB,CAAC,KAAqB;IACtD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA,EAAE,CAAC;IAEtC,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,gBAAgB,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/E,wEAAwE;IACxE,wEAAwE;IACxE,oBAAoB;IACpB,MAAM,YAAY,GAAG,gBAAgB,CAAC,MAAM,GAAG,CAAC,CAAC;IAEjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;QACjD,MAAM,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,YAAY;YAC1B,CAAC,CAAC,IAAI,CAAA,yCAAyC,UAAU,KAAK,UAAU,8BAA8B,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,cAAc;YAC9I,CAAC,CAAC,IAAI,CAAA,EAAE,CAAC;QACX,OAAO,IAAI,CAAA,GAAG,OAAO,qCAAqC,UAAU,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC;IACzI,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,CAAA;;QAEL,oBAAoB,CAAC,eAAe,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC;QAC5D,mBAAmB,CAAC,gBAAgB,CAAC;gCACb,MAAM;;;GAGnC,CAAC;AACJ,CAAC;AAED,SAAS,mBAAmB,CAAC,gBAAuC;IAClE,MAAM,KAAK,GAAe;QACxB,IAAI,CAAA,uGAAuG;KAC5G,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CACR,IAAI,CAAA,qDAAqD,CAAC,6BAA6B,CAAC,0BAA0B,CAAC,WAAW,CAC/H,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAA;;+EAEkE,KAAK;;;;;;;;;;;;GAYjF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,IAAkB,EAAE,UAAsB;IAChE,MAAM,MAAM,GAAG,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAsD,CAAC;IAEhF,wEAAwE;IACxE,MAAM,KAAK,GAAe,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;IAC/D,IAAI,WAAW,EAAE,aAAa;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC;IAClF,IAAI,IAAI,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,IAAI,IAAI,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IAErD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS;QAC3B,CAAC,CAAC,IAAI,CAAA,gCAAgC,IAAI,CAAC,SAAS,gDAAgD,IAAI,CAAC,IAAI,qBAAqB;QAClI,CAAC,CAAC,IAAI,CAAA,EAAE,CAAC;IAEX,MAAM,UAAU,GACd,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QAC/B,CAAC,CAAC,IAAI,CAAA,mCAAmC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,+CAA+C,IAAI,CAAC,IAAI,CAAC,GAAG,CACrH,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAA,6BAA6B,gBAAgB,CAAC,KAAK,CAAC,SAAS,CAC7E,SAAS;QACZ,CAAC,CAAC,IAAI,CAAA,EAAE,CAAC;IAEb,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW;QACpC,CAAC,CAAC,IAAI,CAAA;;;uBAGa,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;;OAEzD;QACH,CAAC,CAAC,IAAI,CAAA,EAAE,CAAC;IAEX,MAAM,UAAU,GAAG,IAAI,CAAA;;;QAGjB,aAAa,CAAC,QAAQ,IAAI,CAAC,IAAI,EAAE,EAAE,sBAAsB,CAAC,IAAI,CAAC,CAAC;;GAErE,CAAC;IAEF,yEAAyE;IACzE,yEAAyE;IACzE,4EAA4E;IAC5E,uCAAuC;IACvC,MAAM,YAAY,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAE3F,OAAO,IAAI,CAAA;;;YAGD,MAAM;;yBAEO,UAAU;mBAChB,IAAI,CAAC,IAAI;qBACP,YAAY;;;2CAGU,MAAM,KAAK,IAAI,CAAC,IAAI;4CACnB,KAAK;UACvC,MAAM;;6BAEa,IAAI,CAAC,WAAW;;UAEnC,UAAU;;YAER,UAAU;YACV,aAAa;;;;GAItB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,IAAkB;IAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,WAAgF,CAAC;IAChG,IAAI,CAAC,EAAE,YAAY,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAC5C,IAAI,CAAC,EAAE,eAAe,KAAK,IAAI;QAAE,OAAO,aAAa,CAAC;IACtD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAqB;IAC/C,MAAM,OAAO,GAAuC,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAC7F,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,KAAa;IACrC,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAkB;IAChD,MAAM,IAAI,GAA4B,EAAE,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,KAAK,GAAG,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CACnB;QACE,OAAO,EAAE,KAAK;QACd,EAAE,EAAE,CAAC;QACL,MAAM,EAAE,YAAY;QACpB,MAAM,EAAE;YACN,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,SAAS,EAAE,IAAI;SAChB;KACF,EACD,IAAI,EACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/mcp-ts-core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"mcpName": "io.github.cyanheads/mcp-ts-core",
|
|
5
5
|
"description": "Agent-native TypeScript framework for building MCP servers. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.",
|
|
6
6
|
"main": "dist/core/index.js",
|
|
@@ -108,8 +108,10 @@ const APP_HTML = `<!DOCTYPE html>
|
|
|
108
108
|
<!-- your UI markup -->
|
|
109
109
|
|
|
110
110
|
<script type="module">
|
|
111
|
-
//
|
|
112
|
-
//
|
|
111
|
+
// PROTOTYPING ONLY — replace before shipping. Bundle via Vite +
|
|
112
|
+
// vite-plugin-singlefile or inline the SDK. Live CDN imports require
|
|
113
|
+
// CSP whitelisting, add supply-chain risk, and break offline use.
|
|
114
|
+
// See UI Notes below.
|
|
113
115
|
import {
|
|
114
116
|
App,
|
|
115
117
|
applyDocumentTheme,
|
|
@@ -186,13 +188,27 @@ export const {{RESOURCE_EXPORT}} = appResource('ui://{{tool-name}}/app.html', {
|
|
|
186
188
|
});
|
|
187
189
|
```
|
|
188
190
|
|
|
189
|
-
## UI
|
|
191
|
+
## UI Notes
|
|
190
192
|
|
|
191
|
-
- **
|
|
192
|
-
- **
|
|
193
|
-
- **
|
|
194
|
-
-
|
|
195
|
-
-
|
|
193
|
+
- **Ship self-contained HTML.** Author with Vite + `vite-plugin-singlefile` or inline the SDK. Live CDN imports in a `ui://` resource are a CSP footgun (every domain has to be whitelisted on `_meta.ui.csp.resourceDomains`), a supply-chain footgun (third-party JS executes inside the host's iframe), and a runtime footgun (every render needs network). The `unpkg` line in the template is for prototyping only.
|
|
194
|
+
- **CSP.** MCP Apps iframes run under deny-by-default CSP. With `appResource()`, put `_meta.ui.csp.resourceDomains` on the definition; the builder mirrors it into returned `resources/read` content items. With plain `resource()`, attach `_meta.ui` yourself in `format()`.
|
|
195
|
+
- **Adopt the host's visual identity, don't impose your own.** App UIs render inside the host's iframe alongside its native UI. Three host hooks layer on top of your CSS:
|
|
196
|
+
- `applyDocumentTheme(hostContext.theme)` — sets `color-scheme` and a `data-theme` attribute on `<html>`
|
|
197
|
+
- `applyHostStyleVariables(hostContext.styles.variables)` — installs host CSS custom properties on `:root` (host decides the names, e.g. `--mcp-color-bg-primary`)
|
|
198
|
+
- `applyHostFonts(hostContext.styles.css.fonts)` — installs `@font-face` rules for the host's font stack
|
|
199
|
+
|
|
200
|
+
Author CSS to *consume* these via `var(--mcp-color-bg-primary, /* fallback */ #fff)`. Don't hardcode brand colors that fight the host.
|
|
201
|
+
- **Pre-connect baseline.** `app.connect()` is async — host context arrives a frame or two after first paint. Without a baseline, the UI flashes unstyled or wrong-themed on light hosts. Ship a `prefers-color-scheme`-aware default so the first frame is sensible:
|
|
202
|
+
|
|
203
|
+
```css
|
|
204
|
+
:root { color-scheme: light dark; --bg: #fff; --fg: #111; }
|
|
205
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0c0d12; --fg: #ededef; } }
|
|
206
|
+
body { background: var(--bg); color: var(--fg); }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Host vars override these once `onhostcontextchanged` fires.
|
|
210
|
+
- **`format()` for app tools.** The first `text` content block is typically JSON that the UI parses via `ontoolresult`. Additional blocks are the human-readable fallback that non-app hosts and LLMs consume — they must render every field the LLM needs to reason about. JSON-only payloads leave model-visible context blind.
|
|
211
|
+
- **App resource `format()`.** `appResource()` already preserves raw HTML for the default app MIME type and mirrors definition `_meta.ui` into content items. Add a custom `format()` only when you need extra per-read metadata or non-default content shaping.
|
|
196
212
|
|
|
197
213
|
## Registration
|
|
198
214
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Scaffold a new service integration. Use when the user asks to add a service, integrate an external API, or create a reusable domain module with its own initialization and state.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -234,6 +234,12 @@ Services don't declare `errors: [...]` contracts and don't have `ctx.fail` — t
|
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
- **Tool/resource handlers bubble service errors unchanged** — the contract advertises the *advertised* failure surface, and any code thrown from a service still reaches the client correctly via the auto-classifier. The conformance lint scans handler source text only, so service-thrown codes aren't flagged.
|
|
237
|
+
- **Carry contract `reason` via `data: { reason }`** when the calling tool declares an `errors[]` contract entry for this failure mode. Services can't call `ctx.fail`, but passing the reason in `data` flows through the auto-classifier untouched, so clients see the same `error.data.reason` they'd see from `ctx.fail` — no handler-side catch-and-rethrow needed:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// tool declares: errors: [{ reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError, when: '…' }]
|
|
241
|
+
throw validationError('Expression cannot be empty.', { reason: 'empty_expression' });
|
|
242
|
+
```
|
|
237
243
|
|
|
238
244
|
## API Efficiency
|
|
239
245
|
|
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: "1.
|
|
7
|
+
version: "1.9"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -269,6 +269,8 @@ export const fetchArticles = tool('fetch_articles', {
|
|
|
269
269
|
|
|
270
270
|
`ctx.fail` accepts an optional 4th `options` argument for ES2022 cause chaining: `throw ctx.fail('upstream_error', 'Upstream returned 500', { url }, { cause: e })`.
|
|
271
271
|
|
|
272
|
+
**Service-thrown contract reasons.** When the throw happens in a called service rather than the handler itself, `ctx.fail` isn't reachable — services don't have `ctx`. Pass `data: { reason: 'X' }` to the factory in the service; the framework's auto-classifier preserves `data` on the wire, so the contract reason rides through unchanged. The handler bubbles the error without catching. See `add-service` for the pattern.
|
|
273
|
+
|
|
272
274
|
**Fallback: error factories.** Use when no contract entry fits — ad-hoc throws, prototype tools, or service-layer code. The framework also auto-classifies plain `throw new Error()` from message patterns as a last resort.
|
|
273
275
|
|
|
274
276
|
```typescript
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.1"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -69,6 +69,26 @@ export const fetchTool = tool('fetch_articles', {
|
|
|
69
69
|
|
|
70
70
|
> **Limits of the conformance lint.** The conformance and prefer-fail rules scan the handler's source text for `throw` statements. Errors thrown from called services (e.g. `await myService.fetch()` raising `RateLimited` internally) are invisible — the lint only sees what's lexically in the handler. Treat the contract as the *advertised* failure surface; bubbled-up codes still reach the client correctly via the auto-classifier, just without lint enforcement.
|
|
71
71
|
|
|
72
|
+
### Carrying contract `reason` from services
|
|
73
|
+
|
|
74
|
+
Services don't have `ctx`, so they can't call `ctx.fail`. To make a service-thrown failure carry the contract's `reason` on the wire, **pass `data: { reason: 'X' }` to the factory**. The framework's auto-classifier preserves `data` unchanged, so clients see the same `error.data.reason` they'd see from `ctx.fail`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// my-service.ts
|
|
78
|
+
throw validationError('Expression cannot be empty.', { reason: 'empty_expression' });
|
|
79
|
+
throw serviceUnavailable('Upstream timeout', { reason: 'evaluation_timeout' });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// my-tool.tool.ts
|
|
84
|
+
errors: [
|
|
85
|
+
{ reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError, when: 'Input is empty.' },
|
|
86
|
+
{ reason: 'evaluation_timeout', code: JsonRpcErrorCode.ServiceUnavailable, when: 'Upstream exceeded the configured timeout.' },
|
|
87
|
+
]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The handler doesn't catch and re-throw — letting service errors bubble unchanged keeps "logic throws, framework catches" intact. The contract still publishes in `tools/list`, the wire payload still carries `code` + `data.reason`, and clients can switch on reason without parsing message text. What's lost is lint-time enforcement that every reason is reachable; compensate with one wire-shape test per reason.
|
|
91
|
+
|
|
72
92
|
---
|
|
73
93
|
|
|
74
94
|
## Error Factories (fallback)
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.1"
|
|
8
8
|
audience: external
|
|
9
9
|
type: debug
|
|
10
10
|
---
|
|
@@ -27,14 +27,20 @@ Write the helper to `/tmp/mcp-field-test.sh` once, then source it in every subse
|
|
|
27
27
|
cat > /tmp/mcp-field-test.sh <<'HELPER_EOF'
|
|
28
28
|
#!/bin/bash
|
|
29
29
|
# Field-test helper: manage an MCP HTTP server + JSON-RPC session across shell calls.
|
|
30
|
+
# Surfaces failures aggressively — field test is for finding things that fail,
|
|
31
|
+
# so the helper auto-tails logs and prints HTTP status/body on errors instead
|
|
32
|
+
# of swallowing them.
|
|
30
33
|
STATE_FILE="/tmp/mcp-field-test.env"
|
|
31
34
|
[ -f "$STATE_FILE" ] && . "$STATE_FILE"
|
|
32
35
|
|
|
33
36
|
mcp_start() {
|
|
34
37
|
local dir="${1:-$PWD}"
|
|
35
38
|
echo "building $dir ..."
|
|
36
|
-
(cd "$dir" && bun run rebuild) >/tmp/mcp-build.log 2>&1
|
|
37
|
-
|
|
39
|
+
if ! (cd "$dir" && bun run rebuild) >/tmp/mcp-build.log 2>&1; then
|
|
40
|
+
echo "BUILD FAILED — last 30 lines of /tmp/mcp-build.log:"
|
|
41
|
+
tail -30 /tmp/mcp-build.log
|
|
42
|
+
return 1
|
|
43
|
+
fi
|
|
38
44
|
echo "starting server ..."
|
|
39
45
|
(cd "$dir" && bun run start:http) >/tmp/mcp-server.log 2>&1 &
|
|
40
46
|
local pid=$!
|
|
@@ -45,7 +51,8 @@ mcp_start() {
|
|
|
45
51
|
sleep 0.25
|
|
46
52
|
done
|
|
47
53
|
if [ -z "$line" ]; then
|
|
48
|
-
echo "server failed to start —
|
|
54
|
+
echo "server failed to start within 10s — last 30 lines of /tmp/mcp-server.log:"
|
|
55
|
+
tail -30 /tmp/mcp-server.log
|
|
49
56
|
kill "$pid" 2>/dev/null
|
|
50
57
|
return 1
|
|
51
58
|
fi
|
|
@@ -63,12 +70,21 @@ EOF
|
|
|
63
70
|
mcp_init() {
|
|
64
71
|
[ -z "$MCP_URL" ] && { echo "run mcp_start first"; return 1; }
|
|
65
72
|
local hdr="/tmp/mcp-init-headers.txt"
|
|
66
|
-
|
|
73
|
+
local body_file="/tmp/mcp-init-body.txt"
|
|
74
|
+
local status
|
|
75
|
+
status=$(curl -sS -D "$hdr" -o "$body_file" -w '%{http_code}' -X POST "$MCP_URL" \
|
|
67
76
|
-H "Content-Type: application/json" \
|
|
68
77
|
-H "Accept: application/json, text/event-stream" \
|
|
69
|
-
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"field-test","version":"2.
|
|
78
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"field-test","version":"2.1"}}}')
|
|
70
79
|
local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
|
|
71
|
-
[ -z "$sid" ]
|
|
80
|
+
if [ -z "$sid" ]; then
|
|
81
|
+
echo "init failed — HTTP $status, no Mcp-Session-Id header returned"
|
|
82
|
+
echo "--- response body ---"
|
|
83
|
+
cat "$body_file"
|
|
84
|
+
echo "--- response headers ---"
|
|
85
|
+
cat "$hdr"
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
72
88
|
cat > "$STATE_FILE" <<EOF
|
|
73
89
|
export MCP_PID=$MCP_PID
|
|
74
90
|
export MCP_URL=$MCP_URL
|
|
@@ -81,11 +97,13 @@ EOF
|
|
|
81
97
|
-H "Accept: application/json, text/event-stream" \
|
|
82
98
|
-H "Mcp-Session-Id: $sid" \
|
|
83
99
|
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
|
|
84
|
-
echo "session=$sid"
|
|
100
|
+
echo "session=$sid (HTTP $status)"
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
# Usage: mcp_call METHOD [JSON_PARAMS]
|
|
88
|
-
# Prints the JSON-RPC response
|
|
104
|
+
# Prints the JSON-RPC response. SSE framing is stripped when present; on
|
|
105
|
+
# non-SSE responses the raw body is printed instead so plain-JSON error
|
|
106
|
+
# replies (HTTP 4xx/5xx) still surface. Pipe to `jq`.
|
|
89
107
|
mcp_call() {
|
|
90
108
|
[ -z "$MCP_SID" ] && { echo "run mcp_init first"; return 1; }
|
|
91
109
|
local method="$1"; local params="${2:-}"
|
|
@@ -95,17 +113,57 @@ mcp_call() {
|
|
|
95
113
|
else
|
|
96
114
|
body=$(printf '{"jsonrpc":"2.0","id":%d,"method":"%s","params":%s}' "$RANDOM" "$method" "$params")
|
|
97
115
|
fi
|
|
98
|
-
|
|
116
|
+
local resp_file="/tmp/mcp-call-body.txt"
|
|
117
|
+
local status
|
|
118
|
+
status=$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST "$MCP_URL" \
|
|
99
119
|
-H "Content-Type: application/json" \
|
|
100
120
|
-H "Accept: application/json, text/event-stream" \
|
|
101
121
|
-H "Mcp-Session-Id: $MCP_SID" \
|
|
102
|
-
-d "$body"
|
|
122
|
+
-d "$body")
|
|
123
|
+
if [ "$status" -ge 400 ]; then
|
|
124
|
+
echo "HTTP $status from $method — response:" >&2
|
|
125
|
+
cat "$resp_file" >&2
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
local sse; sse=$(sed -n 's/^data: //p' "$resp_file")
|
|
129
|
+
if [ -n "$sse" ]; then
|
|
130
|
+
printf '%s\n' "$sse"
|
|
131
|
+
else
|
|
132
|
+
cat "$resp_file"
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Tail the server log. Useful when a call surprises you — pino startup banner,
|
|
137
|
+
# definition lint diagnostics, request handler errors, upstream calls, and
|
|
138
|
+
# rate-limit warnings live in /tmp/mcp-server.log.
|
|
139
|
+
# Usage: mcp_log [N] (default: 50 lines)
|
|
140
|
+
mcp_log() {
|
|
141
|
+
local n="${1:-50}"
|
|
142
|
+
tail -n "$n" /tmp/mcp-server.log
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
mcp_stop() {
|
|
106
|
-
[ -
|
|
146
|
+
if [ -z "$MCP_PID" ]; then
|
|
147
|
+
rm -f "$STATE_FILE"
|
|
148
|
+
echo "no PID to stop"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
kill "$MCP_PID" 2>/dev/null
|
|
152
|
+
for _ in $(seq 1 12); do
|
|
153
|
+
kill -0 "$MCP_PID" 2>/dev/null || break
|
|
154
|
+
sleep 0.25
|
|
155
|
+
done
|
|
156
|
+
if kill -0 "$MCP_PID" 2>/dev/null; then
|
|
157
|
+
echo "PID $MCP_PID didn't exit on SIGTERM — sending SIGKILL"
|
|
158
|
+
kill -9 "$MCP_PID" 2>/dev/null
|
|
159
|
+
sleep 0.5
|
|
160
|
+
fi
|
|
161
|
+
if kill -0 "$MCP_PID" 2>/dev/null; then
|
|
162
|
+
echo "WARNING: PID $MCP_PID still alive after SIGKILL"
|
|
163
|
+
else
|
|
164
|
+
echo "stopped pid=$MCP_PID"
|
|
165
|
+
fi
|
|
107
166
|
rm -f "$STATE_FILE"
|
|
108
|
-
echo "stopped"
|
|
109
167
|
}
|
|
110
168
|
HELPER_EOF
|
|
111
169
|
|
|
@@ -190,6 +248,8 @@ Use `TaskCreate` — one task per definition. Mark complete as you go. Don't bat
|
|
|
190
248
|
|
|
191
249
|
For each call, capture: input sent, response (trim huge payloads to files), whether `isError: true` appeared, anything surprising (slow response, parity drift, unhelpful text, crash).
|
|
192
250
|
|
|
251
|
+
When a call surprises you — slow, hangs, returns terse output, surfaces an unhelpful error — run `. /tmp/mcp-field-test.sh && mcp_log` to tail the server log. The pino startup banner, request handler errors, upstream API call traces, and rate-limit warnings all land in `/tmp/mcp-server.log` rather than coming back through `mcp_call`. Don't guess at runtime behavior from response text alone.
|
|
252
|
+
|
|
193
253
|
**Interpreting responses**
|
|
194
254
|
|
|
195
255
|
- Tool domain errors return `{result: {content: [...], isError: true}}` — they live in `result`, not `error`. Check `isError`, not the JSON-RPC error field.
|