@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.
Files changed (30) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/changelog/0.8.x/0.8.0.md +17 -15
  4. package/changelog/0.8.x/0.8.1.md +17 -0
  5. package/changelog/0.8.x/0.8.2.md +18 -0
  6. package/changelog/template.md +13 -0
  7. package/dist/logs/combined.log +4 -4
  8. package/dist/logs/error.log +4 -4
  9. package/dist/mcp-server/transports/http/landing-page/assets/copy-script.d.ts +13 -4
  10. package/dist/mcp-server/transports/http/landing-page/assets/copy-script.d.ts.map +1 -1
  11. package/dist/mcp-server/transports/http/landing-page/assets/copy-script.js +99 -25
  12. package/dist/mcp-server/transports/http/landing-page/assets/copy-script.js.map +1 -1
  13. package/dist/mcp-server/transports/http/landing-page/assets/styles.d.ts.map +1 -1
  14. package/dist/mcp-server/transports/http/landing-page/assets/styles.js +318 -8
  15. package/dist/mcp-server/transports/http/landing-page/assets/styles.js.map +1 -1
  16. package/dist/mcp-server/transports/http/landing-page/sections/status-strip.d.ts.map +1 -1
  17. package/dist/mcp-server/transports/http/landing-page/sections/status-strip.js +20 -1
  18. package/dist/mcp-server/transports/http/landing-page/sections/status-strip.js.map +1 -1
  19. package/dist/mcp-server/transports/http/landing-page/sections/tools.d.ts +6 -5
  20. package/dist/mcp-server/transports/http/landing-page/sections/tools.d.ts.map +1 -1
  21. package/dist/mcp-server/transports/http/landing-page/sections/tools.js +114 -69
  22. package/dist/mcp-server/transports/http/landing-page/sections/tools.js.map +1 -1
  23. package/package.json +1 -1
  24. package/skills/add-app-tool/SKILL.md +24 -8
  25. package/skills/add-service/SKILL.md +7 -1
  26. package/skills/add-tool/SKILL.md +3 -1
  27. package/skills/api-errors/SKILL.md +21 -1
  28. package/skills/field-test/SKILL.md +73 -13
  29. package/skills/maintenance/SKILL.md +22 -7
  30. package/templates/changelog/template.md +18 -5
@@ -1,40 +1,73 @@
1
1
  /**
2
- * @fileoverview Tools section — responsive 2-column card grid, optionally
3
- * prefix-grouped when ≥2 tools share a common `snake_case` prefix. Each card
4
- * carries annotation pills (read-only / destructive / open-world / task /
5
- * app), auth scope chips, a JSON-RPC invocation snippet, and a collapsible
6
- * input-schema preview.
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 groups = groupToolsByPrefix(tools);
16
- // A single group whether labeled or not — would render as redundant with
17
- // the section header. Skip the sub-heading; render a flat grid.
18
- const showHeadings = groups.length > 1;
19
- const body = groups.map((group) => {
20
- const heading = showHeadings && group.label ? html `<h4 class="group-heading">${group.label}</h4>` : html ``;
21
- return html `${heading}<div class="card-grid">${group.tools.map(renderToolCard)}</div>`;
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
- ${body}
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 renderToolCard(tool) {
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
- const pills = [];
34
- if (annotations?.readOnlyHint)
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>Input schema</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>Invocation</summary>
92
+ <details class="card-detail">
93
+ <summary>invocation</summary>
58
94
  ${renderSnippet(`tool-${tool.name}`, buildInvocationSnippet(tool))}
59
95
  </details>
60
96
  `;
61
- const authBadges = tool.auth && tool.auth.length > 0
62
- ? html `<div class="card-meta"><span class="card-meta-label">scopes</span>${tool.auth.map((scope) => html ` <span class="pill pill-auth">${scope}</span>`)}</div>`
63
- : html ``;
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 class="card" id="${anchor}">
66
- <div class="card-head">
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
- </div>
115
+ </header>
71
116
  <p class="card-desc">${tool.description}</p>
72
- ${authBadges}
73
- ${invocation}
74
- ${schemaPreview}
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
- function groupToolsByPrefix(tools) {
79
- if (tools.length < 3)
80
- return [{ label: null, tools }];
81
- const prefixCounts = new Map();
82
- for (const tool of tools) {
83
- const prefix = tool.name.split('_', 1)[0];
84
- if (!prefix)
85
- continue;
86
- prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
87
- }
88
- const groupablePrefixes = new Set([...prefixCounts.entries()].filter(([, count]) => count >= 2).map(([p]) => p));
89
- if (groupablePrefixes.size === 0)
90
- return [{ label: null, tools }];
91
- const groups = new Map();
92
- const other = [];
93
- for (const tool of tools) {
94
- const prefix = tool.name.split('_', 1)[0];
95
- if (prefix && groupablePrefixes.has(prefix)) {
96
- const list = groups.get(prefix) ?? [];
97
- list.push(tool);
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
- function titleCase(s) {
113
- return s.charAt(0).toUpperCase() + s.slice(1);
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":"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"}
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../../../../../src/mcp-server/transports/http/landing-page/sections/tools.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.8.0",
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
- // Prefer a bundled or inlined SDK for the final shipped HTML. Leaving a live
112
- // CDN import in the served ui:// resource is not the recommended default.
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 Design Notes
191
+ ## UI Notes
190
192
 
191
- - **Bundling:** Prefer Vite + `vite-plugin-singlefile` for any UI that uses `@modelcontextprotocol/ext-apps`. The served `ui://` HTML should ideally be self-contained. The inline template literal pattern is fine for zero-dependency UIs or when you inline the SDK yourself.
192
- - **Client-side SDK:** Author against `@modelcontextprotocol/ext-apps`, but ship a bundled or inlined artifact when possible. Avoid relying on a live CDN import as the default final pattern for portable host compatibility.
193
- - **CSP:** MCP Apps iframes run under deny-by-default CSP. With `appResource()`, put `_meta.ui.csp.resourceDomains` on the definition and the builder will mirror it into returned `resources/read` content items. With plain `resource()`, you still need to attach `_meta.ui` yourself in `format()`.
194
- - **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.
195
- - **format() for app tools:** The first `text` content block is typically JSON that the UI parses via `ontoolresult`. Additional blocks provide a human-readable fallback that non-app hosts and LLMs consume. Do not rely on the JSON block alone for model-visible detail; the fallback blocks still need to render the fields the LLM must reason about.
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.3"
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
 
@@ -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.8"
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.0"
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.0"
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
- || { echo "BUILD FAILED — see /tmp/mcp-build.log"; return 1; }
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 — see /tmp/mcp-server.log"
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
- curl -sS -D "$hdr" -X POST "$MCP_URL" \
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.0"}}}' >/dev/null
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" ] && { echo "no session id returned"; return 1; }
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 (SSE framing stripped). Pipe to `jq`.
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
- curl -sS -X POST "$MCP_URL" \
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" | sed -n 's/^data: //p'
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
- [ -n "$MCP_PID" ] && kill "$MCP_PID" 2>/dev/null
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.