@tsrx/mcp 0.0.3 → 0.0.4

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/README.md CHANGED
@@ -30,8 +30,9 @@ stdio command. This monorepo includes a deployment-neutral endpoint app in
30
30
  `website-mcp` that serves the same MCP server at `/mcp`.
31
31
 
32
32
  The hosted endpoint runs in remote-safe mode. It exposes documentation, prompts,
33
- `format-tsrx`, `compile-tsrx`, and `analyze-tsrx`, but omits local filesystem
34
- tools such as `inspect-project`, `detect-target`, and `validate-tsrx-file`.
33
+ `format-tsrx`, `compile-tsrx`, `analyze-tsrx`, and source-level authoring review
34
+ tools, but omits local filesystem tools such as `inspect-project`,
35
+ `detect-target`, and `validate-tsrx-file`.
35
36
 
36
37
  Set `TSRX_MCP_BEARER_TOKEN` in the endpoint environment to require bearer-token
37
38
  auth. Set `TSRX_MCP_CORS_ORIGIN` to restrict CORS for browser-based clients.
@@ -85,6 +86,13 @@ Add the generic config above to your Codex MCP configuration.
85
86
  - `format-tsrx` - format TSRX code using the official Prettier plugin.
86
87
  - `analyze-tsrx` - compile TSRX code and convert common diagnostics into
87
88
  target-neutral authoring advice with linked docs resources.
89
+ - `review-tsrx-accessibility` - review TSRX source for common accessibility issues
90
+ before browser-based Axe validation, including missing button names, unlabeled
91
+ form controls, and visible text written in a non-rendering shape.
92
+ - `review-tsrx-styles` - review component-scoped style usage for malformed style
93
+ blocks, broad selectors, root styling, and contrast risks.
94
+ - `review-tsrx-components` - review component structure and suggest extraction
95
+ points when control flow, repeated templates, or styles become dense.
88
96
  - `validate-tsrx-file` - read a `.tsrx` file and run formatting, compilation, and
89
97
  diagnostic advice in one read-only pass.
90
98
 
@@ -98,6 +106,11 @@ For generated code, run `format-tsrx` first, then `compile-tsrx` with the inferr
98
106
  or explicit target. If compilation fails, run `analyze-tsrx`, apply the advice,
99
107
  format again, and compile again.
100
108
 
109
+ For user-facing generated UI, run `review-tsrx-accessibility`,
110
+ `review-tsrx-styles`, and `review-tsrx-components` before finalizing. These tools
111
+ do not replace browser validation, but they catch common source-level mistakes
112
+ before the more expensive build, serve, Axe, and visual review loop.
113
+
101
114
  For an existing `.tsrx` file, prefer `validate-tsrx-file`. It reads the file and
102
115
  runs formatting, compilation, and diagnostic advice in one read-only pass.
103
116
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "MCP server for TSRX documentation and project context",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.3",
6
+ "version": "0.0.4",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -37,8 +37,8 @@
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
38
  "prettier": "^3.8.3",
39
39
  "zod": "^4.3.6",
40
- "@tsrx/core": "0.0.22",
41
- "@tsrx/prettier-plugin": "0.3.42"
40
+ "@tsrx/core": "0.0.23",
41
+ "@tsrx/prettier-plugin": "0.3.43"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^24.3.0",
@@ -0,0 +1,489 @@
1
+ /**
2
+ * @typedef {{
3
+ * kind: string,
4
+ * severity: 'error' | 'warning' | 'info',
5
+ * title: string,
6
+ * message: string,
7
+ * snippet: string | null,
8
+ * recommendation: string,
9
+ * documentation: string[],
10
+ * }} AuthoringIssue
11
+ */
12
+
13
+ const DEFAULT_FILENAME = 'Component.tsrx';
14
+
15
+ /**
16
+ * @param {string | undefined} filename
17
+ */
18
+ function normalize_filename(filename) {
19
+ return filename?.trim() || DEFAULT_FILENAME;
20
+ }
21
+
22
+ /**
23
+ * @param {string | undefined} target
24
+ */
25
+ function normalize_target(target) {
26
+ return target?.trim() || null;
27
+ }
28
+
29
+ /**
30
+ * @param {string} value
31
+ */
32
+ function escape_regexp(value) {
33
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
+ }
35
+
36
+ /**
37
+ * @param {string} attrs
38
+ * @param {string} name
39
+ */
40
+ function has_attribute(attrs, name) {
41
+ return new RegExp(`\\b${escape_regexp(name)}\\s*=`).test(attrs);
42
+ }
43
+
44
+ /**
45
+ * @param {string} attrs
46
+ * @param {string} name
47
+ */
48
+ function get_attribute(attrs, name) {
49
+ const match = attrs.match(
50
+ new RegExp(`\\b${escape_regexp(name)}\\s*=\\s*("[^"]*"|'[^']*'|\\{[^}]+\\})`),
51
+ );
52
+ if (!match) return null;
53
+ const value = match[1];
54
+ if (
55
+ (value.startsWith('"') && value.endsWith('"')) ||
56
+ (value.startsWith("'") && value.endsWith("'"))
57
+ ) {
58
+ return value.slice(1, -1);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ /**
64
+ * @param {string} text
65
+ */
66
+ function line_snippet(text) {
67
+ return (
68
+ text
69
+ .split('\n')
70
+ .map((line) => line.trim())
71
+ .find(Boolean)
72
+ ?.slice(0, 240) ?? null
73
+ );
74
+ }
75
+
76
+ /**
77
+ * @param {string} inner
78
+ */
79
+ function has_expression_text(inner) {
80
+ return (
81
+ /\{\s*(['"`])[\s\S]*?\1\s*\}/.test(inner) || /\{[A-Za-z0-9_$.[\]?!:'"`\s+-]+\}/.test(inner)
82
+ );
83
+ }
84
+
85
+ /**
86
+ * @param {string} inner
87
+ */
88
+ function has_direct_quoted_text(inner) {
89
+ return /(^|>)\s*"[^"]+"\s*(<|$)/.test(inner);
90
+ }
91
+
92
+ /**
93
+ * @param {string} inner
94
+ */
95
+ function has_visible_label_text(inner) {
96
+ const without_tags = inner.replace(/<[^>]+>/g, ' ');
97
+ return has_expression_text(inner) || /[A-Za-z0-9]/.test(without_tags.replace(/"[^"]*"/g, ''));
98
+ }
99
+
100
+ /**
101
+ * @param {string} code
102
+ * @param {string} id
103
+ */
104
+ function find_label_for_id(code, id) {
105
+ const quoted = escape_regexp(id);
106
+ const label_pattern = new RegExp(
107
+ `<label\\b([^>]*)\\bhtmlFor\\s*=\\s*["']${quoted}["']([^>]*)>([\\s\\S]*?)<\\/label>`,
108
+ 'g',
109
+ );
110
+ const match = label_pattern.exec(code);
111
+ return match ? match[0] : null;
112
+ }
113
+
114
+ /**
115
+ * @param {string} attrs
116
+ */
117
+ function get_input_type(attrs) {
118
+ return (get_attribute(attrs, 'type') ?? 'text').replace(/[{}'"`]/g, '').toLowerCase();
119
+ }
120
+
121
+ /**
122
+ * @param {string} code
123
+ */
124
+ function collect_direct_quoted_text_issues(code) {
125
+ /** @type {AuthoringIssue[]} */
126
+ const issues = [];
127
+ const direct_text_regex = /<([a-z][\w.-]*)(\s[^>]*)?>\s*"([^"]+)"\s*<\/\1>/g;
128
+ for (const match of code.matchAll(direct_text_regex)) {
129
+ issues.push({
130
+ kind: 'direct-quoted-text',
131
+ severity: match[1] === 'button' ? 'error' : 'warning',
132
+ title: 'Use expression text for visible TSRX text',
133
+ message:
134
+ 'Direct quoted text inside TSRX elements can be treated as a JavaScript string statement instead of rendered text in some target output.',
135
+ snippet: line_snippet(match[0]),
136
+ recommendation: `Replace it with expression text, for example <${match[1]}>{'${match[3].replace(/'/g, "\\'")}'}</${match[1]}>.`,
137
+ documentation: ['tsrx://docs/text-and-template-expressions.md'],
138
+ });
139
+ }
140
+ return issues;
141
+ }
142
+
143
+ /**
144
+ * Reviews generated TSRX source for common accessibility mistakes that are cheap
145
+ * to catch before a browser-based Axe run.
146
+ *
147
+ * @param {{ code: string, filename?: string, target?: string }} input
148
+ */
149
+ export function review_tsrx_accessibility(input) {
150
+ const filename = normalize_filename(input.filename);
151
+ const target = normalize_target(input.target);
152
+ /** @type {AuthoringIssue[]} */
153
+ const issues = [...collect_direct_quoted_text_issues(input.code)];
154
+
155
+ const button_regex = /<button\b([^>]*)>([\s\S]*?)<\/button>/g;
156
+ for (const match of input.code.matchAll(button_regex)) {
157
+ const attrs = match[1] ?? '';
158
+ const inner = match[2] ?? '';
159
+ const has_name =
160
+ has_attribute(attrs, 'aria-label') ||
161
+ has_attribute(attrs, 'aria-labelledby') ||
162
+ has_attribute(attrs, 'title') ||
163
+ (has_expression_text(inner) && !/^\s*\{\s*['"`]\s*['"`]\s*\}\s*$/.test(inner));
164
+
165
+ if (!has_name || has_direct_quoted_text(inner)) {
166
+ issues.push({
167
+ kind: 'button-accessible-name',
168
+ severity: 'error',
169
+ title: 'Give every button a stable accessible name',
170
+ message:
171
+ 'Buttons must expose a visible label or an aria-label. This is especially important for disabled submit buttons and icon-only controls.',
172
+ snippet: line_snippet(match[0]),
173
+ recommendation:
174
+ "Use visible expression text such as {'Add task'} or add aria-label for icon-only buttons.",
175
+ documentation: ['tsrx://docs/text-and-template-expressions.md'],
176
+ });
177
+ }
178
+ }
179
+
180
+ const input_regex = /<input\b([^>]*)\/?>/g;
181
+ for (const match of input.code.matchAll(input_regex)) {
182
+ const attrs = match[1] ?? '';
183
+ const type = get_input_type(attrs);
184
+ if (type === 'hidden' || type === 'submit' || type === 'button') continue;
185
+
186
+ const has_direct_name =
187
+ has_attribute(attrs, 'aria-label') ||
188
+ has_attribute(attrs, 'aria-labelledby') ||
189
+ has_attribute(attrs, 'title');
190
+ if (has_direct_name) continue;
191
+
192
+ const id = get_attribute(attrs, 'id');
193
+ const label = id && !id.startsWith('{') ? find_label_for_id(input.code, id) : null;
194
+ const has_text_label = label ? has_visible_label_text(label) : false;
195
+ const needs_direct_name = type === 'checkbox' || type === 'radio' || !has_text_label;
196
+
197
+ if (needs_direct_name) {
198
+ issues.push({
199
+ kind: 'input-accessible-name',
200
+ severity: 'error',
201
+ title: 'Give every form control a readable label',
202
+ message:
203
+ type === 'checkbox' || type === 'radio'
204
+ ? 'Checkboxes and radios used as visual toggles still need an accessible name. A decorative checkmark is not a label.'
205
+ : 'Inputs need a text label, aria-label, aria-labelledby, or title that remains in the rendered DOM.',
206
+ snippet: line_snippet(match[0]),
207
+ recommendation:
208
+ type === 'checkbox' || type === 'radio'
209
+ ? "Add aria-label={todo.completed ? 'Mark task as incomplete' : 'Mark task as complete'} or include visible label text."
210
+ : 'Add a matching text label with htmlFor, or add aria-label when a visible label is not appropriate.',
211
+ documentation: ['tsrx://docs/components.md'],
212
+ });
213
+ }
214
+ }
215
+
216
+ return {
217
+ ok: issues.every((issue) => issue.severity !== 'error'),
218
+ filename,
219
+ target,
220
+ summary:
221
+ issues.length === 0
222
+ ? 'No source-level TSRX accessibility issues were found. Still run browser-based Axe for final verification.'
223
+ : `Found ${issues.length} source-level accessibility issue${issues.length === 1 ? '' : 's'} to fix before final Axe verification.`,
224
+ issues,
225
+ nextSteps:
226
+ issues.length === 0
227
+ ? ['Run browser-based Axe or the benchmark validation loop.']
228
+ : [
229
+ 'Fix error-severity issues before returning code.',
230
+ 'Prefer expression text for visible labels.',
231
+ 'Run review-tsrx-accessibility again, then compile-tsrx and browser-based Axe.',
232
+ ],
233
+ };
234
+ }
235
+
236
+ /**
237
+ * @param {string} hex
238
+ */
239
+ function hex_to_rgb(hex) {
240
+ const value = hex.replace('#', '');
241
+ const expanded =
242
+ value.length === 3
243
+ ? value
244
+ .split('')
245
+ .map((char) => char + char)
246
+ .join('')
247
+ : value;
248
+ if (!/^[0-9a-f]{6}$/i.test(expanded)) return null;
249
+ const int_value = Number.parseInt(expanded, 16);
250
+ return {
251
+ r: (int_value >> 16) & 255,
252
+ g: (int_value >> 8) & 255,
253
+ b: int_value & 255,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * @param {number} channel
259
+ */
260
+ function relative_channel(channel) {
261
+ const value = channel / 255;
262
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
263
+ }
264
+
265
+ /**
266
+ * @param {{ r: number, g: number, b: number }} rgb
267
+ */
268
+ function luminance(rgb) {
269
+ return (
270
+ 0.2126 * relative_channel(rgb.r) +
271
+ 0.7152 * relative_channel(rgb.g) +
272
+ 0.0722 * relative_channel(rgb.b)
273
+ );
274
+ }
275
+
276
+ /**
277
+ * @param {string} foreground
278
+ * @param {string} background
279
+ */
280
+ function contrast_ratio(foreground, background) {
281
+ const fg = hex_to_rgb(foreground);
282
+ const bg = hex_to_rgb(background);
283
+ if (!fg || !bg) return null;
284
+ const lighter = Math.max(luminance(fg), luminance(bg));
285
+ const darker = Math.min(luminance(fg), luminance(bg));
286
+ return (lighter + 0.05) / (darker + 0.05);
287
+ }
288
+
289
+ /**
290
+ * Reviews TSRX scoped style authoring for patterns that commonly produce invalid
291
+ * CSS, missing root styling, or preventable contrast failures.
292
+ *
293
+ * @param {{ code: string, filename?: string, target?: string }} input
294
+ */
295
+ export function review_tsrx_styles(input) {
296
+ const filename = normalize_filename(input.filename);
297
+ const target = normalize_target(input.target);
298
+ /** @type {AuthoringIssue[]} */
299
+ const issues = [];
300
+ const style_blocks = [...input.code.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/g)];
301
+
302
+ if (style_blocks.length === 0) {
303
+ issues.push({
304
+ kind: 'missing-style-block',
305
+ severity: 'warning',
306
+ title: 'Prefer component-scoped styles',
307
+ message:
308
+ 'Non-trivial TSRX components should usually keep component styles in a scoped <style> block next to the component template.',
309
+ snippet: null,
310
+ recommendation:
311
+ 'Add a <style> block in the component and reserve global CSS for document-level resets.',
312
+ documentation: ['tsrx://docs/style-and-server.md'],
313
+ });
314
+ }
315
+
316
+ for (const style_match of style_blocks) {
317
+ const css = style_match[1] ?? '';
318
+ if (/^\s*\{/.test(css)) {
319
+ issues.push({
320
+ kind: 'style-expression-body',
321
+ severity: 'error',
322
+ title: 'Write CSS directly inside <style>',
323
+ message:
324
+ 'A TSRX <style> block should contain CSS text, not a JavaScript template literal expression.',
325
+ snippet: line_snippet(style_match[0]),
326
+ recommendation: 'Replace <style>{`...`}</style> with <style> ...CSS... </style>.',
327
+ documentation: ['tsrx://docs/style-and-server.md'],
328
+ });
329
+ }
330
+
331
+ if (/(^|[{};,\s])(:global\(\*\)|\*)\s*\{/.test(css)) {
332
+ issues.push({
333
+ kind: 'universal-selector',
334
+ severity: 'warning',
335
+ title: 'Avoid universal selectors in scoped styles',
336
+ message:
337
+ 'Universal selectors make generated examples harder to reason about and can leak broad styling assumptions into nested UI.',
338
+ snippet: line_snippet(css.match(/(:global\(\*\)|\*)\s*\{[^}]*\}/)?.[0] ?? css),
339
+ recommendation: 'Style the component root and named classes instead.',
340
+ documentation: ['tsrx://docs/style-and-server.md'],
341
+ });
342
+ }
343
+
344
+ if (/:scope\s*\{/.test(css)) {
345
+ issues.push({
346
+ kind: 'scope-root-style',
347
+ severity: /:scope\s*\{[\s\S]*\b(background|color|min-height|padding|display)\s*:/.test(css)
348
+ ? 'error'
349
+ : 'warning',
350
+ title: 'Put app-level visuals on an explicit root class',
351
+ message:
352
+ 'For generated app examples, app background, text color, and layout are more robust when attached to the rendered root class, such as .app-shell, rather than only :scope.',
353
+ snippet: line_snippet(css.match(/:scope\s*\{[^}]*\}/)?.[0] ?? ':scope'),
354
+ recommendation:
355
+ 'Move root layout/background/color declarations onto the top-level rendered element class.',
356
+ documentation: ['tsrx://docs/style-and-server.md'],
357
+ });
358
+ }
359
+
360
+ if (/:scope\s*\{[\s\S]*background/.test(css)) {
361
+ const low_contrast_on_white = [...css.matchAll(/color\s*:\s*(#[0-9a-fA-F]{3,6})\b/g)]
362
+ .map((match) => ({ color: match[1], ratio: contrast_ratio(match[1], '#ffffff') }))
363
+ .filter((entry) => entry.ratio !== null && entry.ratio < 4.5);
364
+ if (low_contrast_on_white.length > 0) {
365
+ issues.push({
366
+ kind: 'contrast-risk-with-scope-background',
367
+ severity: 'error',
368
+ title: 'Avoid contrast depending only on :scope background',
369
+ message:
370
+ 'Some text colors in this style block would fail contrast on a default light background if the :scope background does not apply to the rendered root.',
371
+ snippet: `Low-contrast-on-white colors: ${low_contrast_on_white
372
+ .map((entry) => `${entry.color} (${entry.ratio?.toFixed(2)}:1)`)
373
+ .join(', ')}`,
374
+ recommendation:
375
+ 'Place the dark background and base text color on the explicit root class, and choose secondary text colors that pass WCAG contrast there.',
376
+ documentation: ['tsrx://docs/style-and-server.md'],
377
+ });
378
+ }
379
+ }
380
+ }
381
+
382
+ return {
383
+ ok: issues.every((issue) => issue.severity !== 'error'),
384
+ filename,
385
+ target,
386
+ summary:
387
+ issues.length === 0
388
+ ? 'No source-level TSRX style issues were found.'
389
+ : `Found ${issues.length} source-level style issue${issues.length === 1 ? '' : 's'} to review.`,
390
+ issues,
391
+ nextSteps:
392
+ issues.length === 0
393
+ ? ['Run compile-tsrx and browser validation for final CSS behavior.']
394
+ : [
395
+ 'Fix error-severity style issues first.',
396
+ 'Put root layout, background, and text color on an explicit rendered class.',
397
+ 'Run review-tsrx-styles again, then validate in a browser.',
398
+ ],
399
+ };
400
+ }
401
+
402
+ /**
403
+ * @param {string} code
404
+ */
405
+ function get_component_body_line_count(code) {
406
+ const match = code.match(/export\s+component\s+\w+[^{]*\{([\s\S]*)\}\s*$/);
407
+ if (!match) return code.split('\n').length;
408
+ return match[1].split('\n').length;
409
+ }
410
+
411
+ /**
412
+ * Reviews TSRX source for component decomposition opportunities.
413
+ *
414
+ * @param {{ code: string, filename?: string, target?: string }} input
415
+ */
416
+ export function review_tsrx_components(input) {
417
+ const filename = normalize_filename(input.filename);
418
+ const target = normalize_target(input.target);
419
+ /** @type {AuthoringIssue[]} */
420
+ const issues = [];
421
+ const body_lines = get_component_body_line_count(input.code);
422
+ const control_flow_count = (input.code.match(/\b(if|for|switch)\s*\(/g) ?? []).length;
423
+ const element_count = (input.code.match(/<[a-z][\w.-]*(\s|>|\/)/g) ?? []).length;
424
+ const style_line_count = [...input.code.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/g)].reduce(
425
+ (total, match) => total + (match[1] ?? '').split('\n').length,
426
+ 0,
427
+ );
428
+
429
+ if (body_lines > 180 || element_count > 45) {
430
+ issues.push({
431
+ kind: 'large-component',
432
+ severity: 'warning',
433
+ title: 'Split large TSRX components into focused subcomponents',
434
+ message:
435
+ 'The component is large enough that generated code is likely to become harder to repair, test, and review.',
436
+ snippet: `${body_lines} component-body lines, ${element_count} template elements.`,
437
+ recommendation:
438
+ 'Extract cohesive pieces such as Header, StatsSummary, TodoComposer, TodoList, TodoItem, and EmptyState.',
439
+ documentation: ['tsrx://docs/components.md', 'tsrx://docs/control-flow.md'],
440
+ });
441
+ }
442
+
443
+ if (control_flow_count >= 4) {
444
+ issues.push({
445
+ kind: 'control-flow-depth',
446
+ severity: 'info',
447
+ title: 'Use subcomponents around repeated or branching UI',
448
+ message:
449
+ 'Multiple control-flow blocks are fine in TSRX, but deeply branching generated examples become more reliable when repeated item UI is moved into a named component.',
450
+ snippet: `${control_flow_count} control-flow blocks detected.`,
451
+ recommendation:
452
+ 'When a for-loop item contains buttons, labels, conditionals, or several nested elements, extract an item component and pass typed props/callbacks.',
453
+ documentation: ['tsrx://docs/components.md', 'tsrx://docs/control-flow.md'],
454
+ });
455
+ }
456
+
457
+ if (style_line_count > 180) {
458
+ issues.push({
459
+ kind: 'large-style-block',
460
+ severity: 'info',
461
+ title: 'Keep style blocks navigable',
462
+ message:
463
+ 'Very large style blocks make generated components harder to audit for accessibility and responsive behavior.',
464
+ snippet: `${style_line_count} style lines detected.`,
465
+ recommendation:
466
+ 'Group styles by extracted component or reduce decorative styling before adding more behavior.',
467
+ documentation: ['tsrx://docs/style-and-server.md'],
468
+ });
469
+ }
470
+
471
+ return {
472
+ ok: true,
473
+ filename,
474
+ target,
475
+ summary:
476
+ issues.length === 0
477
+ ? 'No component decomposition issues were found.'
478
+ : `Found ${issues.length} component-structure recommendation${issues.length === 1 ? '' : 's'}.`,
479
+ issues,
480
+ nextSteps:
481
+ issues.length === 0
482
+ ? ['Keep related hooks, handlers, and template branches co-located.']
483
+ : [
484
+ 'Extract repeated list items and major page regions into named TSRX components.',
485
+ 'Keep hooks and handlers near the branch or component that uses them.',
486
+ 'Compile after extracting components to confirm target output.',
487
+ ],
488
+ };
489
+ }
@@ -56,9 +56,9 @@ export const documentation_sections = [
56
56
  slug: 'style-and-server',
57
57
  title: 'Style and Server Extensions',
58
58
  use_cases:
59
- '#style, scoped css, #server, server blocks, compile-time identifiers',
59
+ 'style directive, scoped css, module server, submodule imports, compile-time identifiers',
60
60
  content:
61
- '# Style and Server Extensions\n\n`#style` is a compile-time identifier for scoped CSS class names declared in the current module.\n\n```tsx\n<div class={#style.card} />\n```\n\n`#server { ... }` marks a lexical region intended for server compile targets. TSRX parses the block; target compilers decide how to emit or strip it.\n\nSpecification grammar:\n\n```text\nStyleIdentifier :\n #style\n\nStyleAccess :\n #style . IdentifierName\n #style [ StringLiteral ]\n\nServerIdentifier :\n #server\n\nServerBlock :\n #server { StatementListopt }\n\nServerMemberAccess :\n #server . IdentifierName\n```\n\nSource: website-tsrx/src/pages/specification.tsrx#style',
61
+ '# Style and Server Extensions\n\n`{style "className"}` is an attribute-value directive for scoped CSS class names declared in the current module.\n\n```tsx\n<Child className={style "card"} />\n```\n\n`module server { ... }` declares a server-oriented submodule in the Ripple host profile. Import exported functions with `import { load } from server` before use.\n\nSpecification grammar:\n\n```text\nStyleAttributeExpression :\n { style StringLiteral }\n\nSubmoduleDeclaration :\n module Identifier { ModuleItemListopt }\n\nSubmoduleImportDeclaration :\n import ImportClause from Identifier ;\n\n```\n\nSource: website-tsrx/src/pages/specification.tsrx#style',
62
62
  },
63
63
  {
64
64
  slug: 'target-integration',
package/src/http.js CHANGED
@@ -82,8 +82,15 @@ export async function handleTSRXMcpNodeRequest(req, res, options = {}) {
82
82
  });
83
83
 
84
84
  try {
85
+ const req_with_body = /** @type {import('node:http').IncomingMessage & { body?: unknown }} */ (
86
+ req
87
+ );
88
+ const parsed_body = Object.prototype.hasOwnProperty.call(req_with_body, 'body')
89
+ ? req_with_body.body
90
+ : undefined;
91
+
85
92
  await server.connect(transport);
86
- await transport.handleRequest(req, res);
93
+ await transport.handleRequest(req, res, parsed_body);
87
94
  } catch (error) {
88
95
  if (!res.headersSent) {
89
96
  send_json(res, 500, {
package/src/index.js CHANGED
@@ -7,11 +7,19 @@ export {
7
7
  get_documentation_handler,
8
8
  inspect_project_handler,
9
9
  list_sections_handler,
10
+ review_tsrx_accessibility_handler,
11
+ review_tsrx_components_handler,
12
+ review_tsrx_styles_handler,
10
13
  validate_tsrx_file_handler,
11
14
  } from './server.js';
12
15
 
13
16
  export { handleTSRXMcpNodeRequest } from './http.js';
14
17
  export { analyze_tsrx } from './analyze.js';
18
+ export {
19
+ review_tsrx_accessibility,
20
+ review_tsrx_components,
21
+ review_tsrx_styles,
22
+ } from './authoring.js';
15
23
  export { compile_tsrx } from './compile.js';
16
24
  export { format_tsrx } from './format.js';
17
25
  export { inspect_project } from './inspect.js';
package/src/schemas.js CHANGED
@@ -66,6 +66,25 @@ export const advice_schema = z.object({
66
66
  documentation: z.array(z.string()),
67
67
  });
68
68
 
69
+ export const authoring_issue_schema = z.object({
70
+ kind: z.string(),
71
+ severity: z.enum(['error', 'warning', 'info']),
72
+ title: z.string(),
73
+ message: z.string(),
74
+ snippet: z.string().nullable(),
75
+ recommendation: z.string(),
76
+ documentation: z.array(z.string()),
77
+ });
78
+
79
+ export const authoring_review_result_schema = {
80
+ ok: z.boolean(),
81
+ filename: z.string(),
82
+ target: z.string().nullable(),
83
+ summary: z.string(),
84
+ issues: z.array(authoring_issue_schema),
85
+ nextSteps: z.array(z.string()),
86
+ };
87
+
69
88
  export const analysis_result_schema = {
70
89
  ok: z.boolean(),
71
90
  target: z.string().nullable(),
package/src/server.js CHANGED
@@ -13,9 +13,15 @@ import { format_tsrx } from './format.js';
13
13
  import { inspect_project } from './inspect.js';
14
14
  import { validate_tsrx_file } from './validate.js';
15
15
  import { detect_target } from './target.js';
16
+ import {
17
+ review_tsrx_accessibility,
18
+ review_tsrx_components,
19
+ review_tsrx_styles,
20
+ } from './authoring.js';
16
21
  import {
17
22
  TARGET_SCHEMA,
18
23
  analysis_result_schema,
24
+ authoring_review_result_schema,
19
25
  compile_result_schema,
20
26
  format_result_schema,
21
27
  inspect_project_result_schema,
@@ -193,6 +199,27 @@ export function analyze_tsrx_handler(input) {
193
199
  return analyze_tsrx(input);
194
200
  }
195
201
 
202
+ /**
203
+ * @param {{ code: string, filename?: string, target?: string }} input
204
+ */
205
+ export function review_tsrx_accessibility_handler(input) {
206
+ return review_tsrx_accessibility(input);
207
+ }
208
+
209
+ /**
210
+ * @param {{ code: string, filename?: string, target?: string }} input
211
+ */
212
+ export function review_tsrx_styles_handler(input) {
213
+ return review_tsrx_styles(input);
214
+ }
215
+
216
+ /**
217
+ * @param {{ code: string, filename?: string, target?: string }} input
218
+ */
219
+ export function review_tsrx_components_handler(input) {
220
+ return review_tsrx_components(input);
221
+ }
222
+
196
223
  /**
197
224
  * @param {{
198
225
  * code: string,
@@ -270,18 +297,21 @@ function create_tsrx_task_prompt(options) {
270
297
  const compile_step = options.remote
271
298
  ? '7. Before presenting generated .tsrx code as final, call `format-tsrx`, then `compile-tsrx` with an explicit target.'
272
299
  : '7. Before presenting generated .tsrx code as final, call `format-tsrx`, then `compile-tsrx` with the inferred or explicit target.';
300
+ const authoring_step =
301
+ '8. For user-facing UI, call `review-tsrx-accessibility`, `review-tsrx-styles`, and `review-tsrx-components` before finalizing. Fix error-severity accessibility/style issues and use component advice when control flow becomes dense.';
273
302
 
274
303
  return `Use this workflow when helping with TSRX:
275
304
 
276
305
  1. Identify whether the task is about target-neutral TSRX syntax, target runtime behavior, or both.
277
306
  ${project_context_step}
278
307
  3. For syntax uncertainty, use \`list-sections\`, \`get-documentation\`, or read \`tsrx://docs/{slug}.md\`.
279
- 4. Keep core TSRX advice target-neutral: component declarations, statement templates, control flow, TSX expression values, lazy destructuring, style identifiers, and server blocks.
308
+ 4. Keep core TSRX advice target-neutral: component declarations, statement templates, control flow, TSX expression values, lazy destructuring, style identifiers, and submodule declarations.
280
309
  5. Use \`tsrx://targets/{target}.md\` as the handoff point for target-specific responsibilities.
281
310
  ${file_validation_step}
282
311
  ${compile_step}
283
- 8. If \`compile-tsrx\` returns diagnostics, call \`analyze-tsrx\` for targeted authoring advice, fix the code, format it, and compile again.
284
- 9. Do not invent runtime APIs, imports, or bundler configuration from target-neutral TSRX docs. Use target-specific docs, resources, or skills for those details.`;
312
+ ${authoring_step}
313
+ 9. If \`compile-tsrx\` returns diagnostics, call \`analyze-tsrx\` for targeted authoring advice, fix the code, format it, and compile again.
314
+ 10. Do not invent runtime APIs, imports, or bundler configuration from target-neutral TSRX docs. Use target-specific docs, resources, or skills for those details.`;
285
315
  }
286
316
 
287
317
  /**
@@ -564,6 +594,87 @@ export function createTSRXMcpServer(options = {}) {
564
594
  },
565
595
  );
566
596
 
597
+ server.registerTool(
598
+ 'review-tsrx-accessibility',
599
+ {
600
+ title: 'Review TSRX Accessibility',
601
+ description:
602
+ 'Reviews TSRX source for common accessibility issues before browser-based Axe validation, including missing button names, unlabeled form controls, and direct quoted text that may not render as accessible text.',
603
+ inputSchema: {
604
+ code: z.string(),
605
+ filename: z.string().optional(),
606
+ target: TARGET_SCHEMA.optional(),
607
+ },
608
+ outputSchema: authoring_review_result_schema,
609
+ annotations: {
610
+ readOnlyHint: true,
611
+ destructiveHint: false,
612
+ openWorldHint: false,
613
+ },
614
+ },
615
+ async (input) => {
616
+ const output = review_tsrx_accessibility_handler(input);
617
+ return {
618
+ content: [{ type: 'text', text: json_text(output) }],
619
+ structuredContent: output,
620
+ };
621
+ },
622
+ );
623
+
624
+ server.registerTool(
625
+ 'review-tsrx-styles',
626
+ {
627
+ title: 'Review TSRX Styles',
628
+ description:
629
+ 'Reviews TSRX source for style-authoring issues, including malformed style blocks, broad selectors, root styling that should live on explicit classes, and contrast risks.',
630
+ inputSchema: {
631
+ code: z.string(),
632
+ filename: z.string().optional(),
633
+ target: TARGET_SCHEMA.optional(),
634
+ },
635
+ outputSchema: authoring_review_result_schema,
636
+ annotations: {
637
+ readOnlyHint: true,
638
+ destructiveHint: false,
639
+ openWorldHint: false,
640
+ },
641
+ },
642
+ async (input) => {
643
+ const output = review_tsrx_styles_handler(input);
644
+ return {
645
+ content: [{ type: 'text', text: json_text(output) }],
646
+ structuredContent: output,
647
+ };
648
+ },
649
+ );
650
+
651
+ server.registerTool(
652
+ 'review-tsrx-components',
653
+ {
654
+ title: 'Review TSRX Component Structure',
655
+ description:
656
+ 'Reviews TSRX source for component decomposition opportunities when control flow, repeated item templates, or style blocks become large enough to hurt generated-code reliability.',
657
+ inputSchema: {
658
+ code: z.string(),
659
+ filename: z.string().optional(),
660
+ target: TARGET_SCHEMA.optional(),
661
+ },
662
+ outputSchema: authoring_review_result_schema,
663
+ annotations: {
664
+ readOnlyHint: true,
665
+ destructiveHint: false,
666
+ openWorldHint: false,
667
+ },
668
+ },
669
+ async (input) => {
670
+ const output = review_tsrx_components_handler(input);
671
+ return {
672
+ content: [{ type: 'text', text: json_text(output) }],
673
+ structuredContent: output,
674
+ };
675
+ },
676
+ );
677
+
567
678
  if (!remote) {
568
679
  server.registerTool(
569
680
  'inspect-project',