diff --git a/custom/knowledge/mcp/bc-mcp-bridge-must-surface-non-2xx-responses-before-sse-parsing.md b/custom/knowledge/mcp/bc-mcp-bridge-must-surface-non-2xx-responses-before-sse-parsing.md new file mode 100644 index 0000000..5c43cb3 --- /dev/null +++ b/custom/knowledge/mcp/bc-mcp-bridge-must-surface-non-2xx-responses-before-sse-parsing.md @@ -0,0 +1,61 @@ +--- +bc-version: [all] +domain: mcp +keywords: [businesscentral, mcp, bc-mcp-bridge, error-handling, sse, timeout, troubleshooting] +technologies: [al, mcp] +countries: [w1] +application-area: [all] +--- + +# bc-mcp-bridge.js Must Check `!r.ok` Before Any SSE Parsing, Unconditionally + +## Description + +`bc-mcp-bridge.js`'s `forward()` function decides how to parse the BC MCP +endpoint's response body based on its `content-type` header. Before +2026-07-31 it only treated a response as an error when `!r.ok && !text` — +i.e. only when there was no body at all. A non-2xx response WITH a body +(the common case — BC returns structured JSON error objects) fell through +to the content-type branch instead. + +## Incident (2026-07-31) + +BC returned a 400 with a plain JSON error body (`{"Error": {"Message": +"..."}}`) while still labelling the response `content-type: +text/event-stream`. `parseSSE()` only extracts lines starting with `data:` — +a plain JSON body has none, so it returned `[]` silently. The stdin loop's +`for (const out of responses) process.stdout.write(...)` then had nothing to +iterate, so the bridge produced **zero output** — not even to stderr — for a +request that BC had already answered in under 200ms. Claude Code had no +signal to work with and waited out its own 30-second client-side timeout, +which was the only thing the developer actually saw. + +## Rule + +Check `!r.ok` before considering content-type at all, and throw +unconditionally (with the body text included) when it's true. Never let a +non-2xx response reach `parseSSE` — a server is free to mislabel an error +body's content-type, and the client must not depend on that label being +honest. + +## Anti-Pattern + + const ct = r.headers.get("content-type") || ""; + const text = await r.text(); + if (!r.ok && !text) throw new Error(`HTTP ${r.status}`); + return ct.includes("text/event-stream") ? parseSSE(text) : [text.trim()]; + // A 400 WITH a body silently falls through to parseSSE and returns []. + +## Compliant + + const ct = r.headers.get("content-type") || ""; + const text = await r.text(); + if (!r.ok) throw new Error(`HTTP ${r.status}: ${text || "(empty body)"}`); + return ct.includes("text/event-stream") ? parseSSE(text) : [text.trim()]; + +## Scope + +`bc-mcp-bridge.js` specifically, but the underlying principle generalizes to +any stdio MCP bridge that branches parsing logic on a server-supplied +content-type header: validate the HTTP status first, independent of what +the header claims the body's shape is. diff --git a/custom/knowledge/mcp/bc-mcp-company-header-must-match-exact-company-name.md b/custom/knowledge/mcp/bc-mcp-company-header-must-match-exact-company-name.md new file mode 100644 index 0000000..2985df7 --- /dev/null +++ b/custom/knowledge/mcp/bc-mcp-company-header-must-match-exact-company-name.md @@ -0,0 +1,57 @@ +--- +bc-version: [all] +domain: mcp +keywords: [businesscentral, mcp, bc-mcp-bridge, company, header, display-name, config, troubleshooting] +technologies: [al, mcp] +countries: [w1] +application-area: [all] +--- + +# BC MCP `Company` Header Must Be the Exact `Navn` Field, Not `Vist navn` + +## Description + +`~/.bc-mcp.config.json`'s `company` value is sent as the literal `Company` +HTTP header to the BC MCP endpoint. It must exactly match the company's +**`Navn`** field in Business Central's company list — not the **`Vist navn`** +(display name) field. The two are often different strings for the same +company, and BC's own company picker UI shows the display name more +prominently, making it the natural (wrong) one to copy. + +## Incident (2026-07-31) + +CURABIS's own `businesscentral` MCP server failed after "working all +evening" with a generic client-side "connection timed out after 30000ms" — +no useful error surfaced to the developer. Root cause: `company` was set to +`"CURABIS ApS"` (the `Vist navn`), while BC's actual `Navn` field is +`"Curabis ApS"`. BC's own API rejected the mismatched header with a fast, +clear 400 error — but `bc-mcp-bridge.js` had a separate bug +(`bc-mcp-bridge-must-surface-non-2xx-responses-before-sse-parsing`, same +incident) that swallowed the error body, turning a sub-200ms server error +into a 30-second client-side hang with no diagnostic. + +## Verification + +If `businesscentral` MCP fails, check BC's company list page (Virksomheder / +Companies) and compare the `Navn` column — not `Vist navn` — against +`~/.bc-mcp.config.json`'s `company` value, character for character. Do not +assume the value that "looks right" from the picker UI is the one the API +needs. + +## Anti-Pattern + + // WRONG: copied from BC's company switcher, which shows Vist navn + { "company": "CURABIS ApS" } + +## Compliant + + // CORRECT: copied from the Navn column on the company list page + { "company": "Curabis ApS" } + +## Scope + +Every machine with `~/.bc-mcp.config.json` configured — this is a +machine-local file, not something Mode B can fix centrally. The template +(`bc-mcp.config.template.json`) carries an explicit warning about this +distinction as of 2026-07-31, but a machine already onboarded before that +date needs its existing file checked manually. diff --git a/custom/setup/bc-mcp-bridge.js b/custom/setup/bc-mcp-bridge.js index d91a277..cc995da 100644 --- a/custom/setup/bc-mcp-bridge.js +++ b/custom/setup/bc-mcp-bridge.js @@ -92,7 +92,14 @@ async function forward(msg) { const sid = r.headers.get("mcp-session-id"); if (sid) sessionId = sid; const ct = r.headers.get("content-type") || ""; const text = await r.text(); - if (!r.ok && !text) throw new Error(`HTTP ${r.status}`); + // 2026-07-31: BC has returned error bodies as plain JSON while still labelling + // content-type text/event-stream (e.g. "company not found"). parseSSE only + // extracts lines starting with "data:" - a plain JSON error body has none, so + // it silently returned []. The stdin loop then wrote nothing at all, and the + // client (Claude Code) waited out its own 30s timeout instead of seeing the + // real error immediately. Check !r.ok BEFORE any SSE parsing, unconditionally - + // never let a non-2xx response fall through to parseSSE. + if (!r.ok) throw new Error(`HTTP ${r.status}: ${text || "(empty body)"}`); return ct.includes("text/event-stream") ? parseSSE(text) : (text.trim() ? [text.trim()] : []); } diff --git a/custom/setup/curabis-standard.agent.md b/custom/setup/curabis-standard.agent.md index 95eb122..39c5a21 100644 --- a/custom/setup/curabis-standard.agent.md +++ b/custom/setup/curabis-standard.agent.md @@ -205,7 +205,12 @@ If it does NOT exist: 2. Write it to `~/.bc-mcp.config.json` as-is 3. Tell the developer: > "⚠️ `~/.bc-mcp.config.json` er oprettet fra CURABIS-template. - > Åbn filen og erstat `` med din egen secret. + > Udfyld ALLE placeholder-felter (tenant, clientId, client secret, company) + > — ikke kun secret'en. For `company`: brug PRÆCIS firmanavnet fra BC's + > 'Navn'-kolonne på virksomhedslisten, IKKE 'Vist navn' — de to kan være + > forskellige strenge for samme firma (2026-07-31: 'CURABIS ApS' vs. + > 'Curabis ApS' forårsagede et 30-sekunders timeout uden brugbar fejl — + > se `bc-mcp-company-header-must-match-exact-company-name`). > Gem filen — BC MCP er klar når du genstarter Claude Code." #### 3c. bcquality-knowledge, roster agents, find-altool.ps1, MCP registration (v24) diff --git a/custom/setup/machine/bc-mcp.config.template.json b/custom/setup/machine/bc-mcp.config.template.json index 2bfd9f9..699c7cd 100644 --- a/custom/setup/machine/bc-mcp.config.template.json +++ b/custom/setup/machine/bc-mcp.config.template.json @@ -1,6 +1,8 @@ { - "tenantId": "CURABIS-TENANT-ID", + "tenant": "CURABIS-TENANT-ID", "clientId": "CURABIS-CLIENT-ID", "clientSecret": "", - "baseUrl": "https://api.businesscentral.dynamics.com" + "environment": "Production", + "company": "", + "configurationName": "CURABIS_DEV" }