Skip to content

Commit fa3d329

Browse files
authored
fix: repair get_component_html, upgrade SDK 0.5 -> 1.29 and refresh toolchain (#11)
Two pre-existing bugs made get_component_html — the reason this server exists — fail on most real design systems. Both were found while verifying the dependency upgrade against a real Storybook 10 build, and both reproduce on main with the old SDK and Puppeteer. 1. The render check required more than 100 characters of HTML A rendered button is 76 characters. Badges, chips, icons and inputs are smaller still, so the poll loop never accepted them, ran its full 15 x 1s, and blew past the 10s caller timeout. Every compact component — the bulk of a design system — returned "Operation timed out after 10000ms". Content now counts as rendered when it is non-empty and is not Storybook's own no-preview placeholder. The loop is also bounded by a deadline instead of an attempt count that could exceed the caller's budget, and polls every 100ms instead of sleeping a full second before even looking. 2. Concurrent jobs corrupted each other The job queue runs maxConcurrent = 2, but PuppeteerClient shared a single page: two simultaneous fetches navigated the same tab and whichever started second tore the first one's document away. On top of that, getPuppeteerClient() had an async-init race — two callers both saw a null field, both launched a browser, and the second assignment orphaned the first mid-request. Either way one of any two parallel jobs failed with a spurious connection error. Each fetch now gets its own page (closed in a finally, so tabs cannot leak) and the launch caches its in-flight promise rather than the resolved client. Verified against Storybook 10.5.4: before, one job timed out at 10s and the other failed; after, two concurrent extractions both complete in ~2.4s with correct HTML. Toolchain, which is what prompted all of the above: - @modelcontextprotocol/sdk 0.5.0 -> 1.29.0. The server was two years behind on a pre-1.0 SDK; it now negotiates protocol 2025-06-18. No API changes were needed, the low-level Server surface is unchanged. - Node 18 dropped (EOL) — engines, CI matrix now 20/22/24, publish workflow and tsup target follow. - eslint 8 -> 10 with a flat config matching the other repos, typescript-eslint 6 -> 8, vitest 3 -> 4, puppeteer 24 -> 25, node-html-parser 6 -> 9, zod 3 -> 4, @types/node 20 -> 26. - eslint 10's new rules found real defects, fixed rather than silenced: five throw sites discarded the caught error instead of chaining it via `cause`, and isSSLCertificateError() ran String() over an unknown value, so a thrown plain object stringified to "[object Object]" and every certificate check silently missed. - Server version was hardcoded in index.ts and had already drifted (1.1.0 there vs 1.1.1 in package.json); it is now injected from package.json at build. - Coverage: added the lcov reporter and coverage.all, so CI can upload and the denominator is the whole source tree rather than whatever tests imported. Thresholds are set to today's real numbers, which are ~6%. They were 80 but never enforced, because CI ran `vitest run` without --coverage. - server.json plus mcpName and the registry publish workflow, so the package appears in the MCP Registry like mcp-gsheets and openclaw-mcp. - README documents Storybook 7-10 support, verified by building Storybook 10.5.4 and extracting from it. 21 tests pass. The new ones pin the length regression directly: a 76-character button must read as rendered, and a long no-preview placeholder must not.
1 parent 5b285c3 commit fa3d329

17 files changed

Lines changed: 3521 additions & 2316 deletions

.eslintrc.json

Lines changed: 0 additions & 51 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
node-version: [18, 20, 22]
19+
node-version: [20, 22, 24]
2020

2121
steps:
2222
- name: Checkout
@@ -41,14 +41,14 @@ jobs:
4141
run: npm run typecheck
4242

4343
- name: Test
44-
run: npm run test:run
44+
run: npm run test:coverage
4545

4646
- name: Build
4747
run: npm run build
4848

4949
- name: Upload coverage
50-
if: matrix.node-version == 20 && hashFiles('coverage/lcov.info') != ''
51-
uses: codecov/codecov-action@v4
50+
if: matrix.node-version == 20
51+
uses: codecov/codecov-action@v7
5252
with:
5353
token: ${{ secrets.CODECOV_TOKEN }}
5454
files: ./coverage/lcov.info
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Publish to MCP Registry
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
id-token: write
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
18+
- name: Sync server.json version with release tag
19+
run: |
20+
VERSION="${GITHUB_REF_NAME#v}"
21+
if [ -z "$VERSION" ] || [ "$VERSION" = "$GITHUB_REF_NAME" ]; then
22+
VERSION=$(node -p "require('./package.json').version")
23+
fi
24+
echo "Publishing version: $VERSION"
25+
node -e "
26+
const fs = require('fs');
27+
const s = JSON.parse(fs.readFileSync('server.json', 'utf8'));
28+
s.version = '$VERSION';
29+
for (const p of s.packages || []) p.version = '$VERSION';
30+
fs.writeFileSync('server.json', JSON.stringify(s, null, 2) + '\n');
31+
"
32+
cat server.json
33+
34+
- name: Wait for npm package to be available
35+
run: |
36+
VERSION=$(node -p "require('./server.json').version")
37+
echo "Waiting for mcp-design-system-extractor@$VERSION on npm..."
38+
for i in $(seq 1 60); do
39+
if npm view "mcp-design-system-extractor@$VERSION" version 2>/dev/null | grep -q "^$VERSION$"; then
40+
echo "Package available."
41+
exit 0
42+
fi
43+
echo "Not yet (attempt $i/60), sleeping 10s..."
44+
sleep 10
45+
done
46+
echo "Timed out waiting for npm package."
47+
exit 1
48+
49+
- name: Install mcp-publisher
50+
run: |
51+
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" \
52+
| tar xz mcp-publisher
53+
chmod +x mcp-publisher
54+
55+
- name: Login to MCP Registry (GitHub OIDC)
56+
run: ./mcp-publisher login github-oidc
57+
58+
- name: Publish to MCP Registry
59+
run: ./mcp-publisher publish

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- name: Use Node.js 18.x
2222
uses: actions/setup-node@v4
2323
with:
24-
node-version: 18
24+
node-version: 20
2525
registry-url: 'https://registry.npmjs.org/'
2626
cache: 'npm'
2727
always-auth: true

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,22 @@ Connects to Storybook via `/index.json` and `/iframe.html` endpoints. Uses Puppe
257257

258258
## Requirements
259259

260-
- Node.js 18+
260+
- Node.js 20+
261261
- Chrome/Chromium (for Puppeteer)
262-
- Running Storybook instance
262+
- Running Storybook instance (see below for supported versions)
263+
264+
### Supported Storybook versions
265+
266+
**Storybook 7, 8, 9 and 10.** The server reads the story index from
267+
`/index.json`, falling back to `/stories.json`, and renders stories through
268+
`/iframe.html?id=<storyId>` — endpoints that have been stable across all four
269+
major versions.
270+
271+
Storybook 6 and earlier are not supported: they predate `/index.json` and use
272+
a different story-id scheme.
273+
274+
Both a dev server (`npm run storybook`) and a built static Storybook served
275+
over HTTP will work.
263276

264277
## Development
265278

eslint.config.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import eslint from '@eslint/js';
2+
import tseslint from 'typescript-eslint';
3+
import prettierConfig from 'eslint-config-prettier';
4+
import prettierPlugin from 'eslint-plugin-prettier';
5+
import globals from 'globals';
6+
7+
export default tseslint.config(
8+
{
9+
ignores: ['dist/**', 'node_modules/**', '**/*.js', 'scripts/**'],
10+
},
11+
{
12+
files: ['src/**/*.ts'],
13+
extends: [
14+
eslint.configs.recommended,
15+
...tseslint.configs.recommended,
16+
...tseslint.configs.recommendedTypeChecked,
17+
prettierConfig,
18+
],
19+
plugins: {
20+
prettier: prettierPlugin,
21+
},
22+
languageOptions: {
23+
globals: {
24+
...globals.node,
25+
...globals.es2022,
26+
},
27+
parserOptions: {
28+
project: './tsconfig.json',
29+
sourceType: 'module',
30+
},
31+
},
32+
rules: {
33+
'prettier/prettier': 'error',
34+
'@typescript-eslint/explicit-function-return-type': 'off',
35+
'@typescript-eslint/explicit-module-boundary-types': 'off',
36+
'@typescript-eslint/no-explicit-any': 'off',
37+
'@typescript-eslint/no-unsafe-assignment': 'off',
38+
'@typescript-eslint/no-unsafe-member-access': 'off',
39+
'@typescript-eslint/no-unsafe-call': 'off',
40+
'@typescript-eslint/no-unsafe-return': 'off',
41+
'@typescript-eslint/no-unsafe-argument': 'off',
42+
'@typescript-eslint/ban-ts-comment': 'off',
43+
'@typescript-eslint/no-unused-vars': [
44+
'error',
45+
{
46+
argsIgnorePattern: '^_',
47+
varsIgnorePattern: '^_',
48+
},
49+
],
50+
'@typescript-eslint/no-floating-promises': 'error',
51+
'@typescript-eslint/no-misused-promises': 'error',
52+
'@typescript-eslint/await-thenable': 'error',
53+
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
54+
'@typescript-eslint/no-empty-object-type': 'off',
55+
'@typescript-eslint/require-await': 'off',
56+
'@typescript-eslint/prefer-nullish-coalescing': 'off',
57+
'@typescript-eslint/prefer-optional-chain': 'warn',
58+
'no-console': ['warn', { allow: ['error', 'warn'] }],
59+
'prefer-const': 'error',
60+
'no-var': 'error',
61+
eqeqeq: ['error', 'always'],
62+
curly: ['error', 'all'],
63+
},
64+
},
65+
);

0 commit comments

Comments
 (0)