@tsrx/mcp 0.0.3 → 0.0.5
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 +15 -2
- package/package.json +3 -3
- package/src/analyze.js +40 -1
- package/src/authoring.js +489 -0
- package/src/generated/docs.js +3 -3
- package/src/http.js +8 -1
- package/src/index.js +8 -0
- package/src/schemas.js +19 -0
- package/src/server.js +115 -3
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`,
|
|
34
|
-
tools such as `inspect-project`,
|
|
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.
|
|
6
|
+
"version": "0.0.5",
|
|
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.
|
|
41
|
-
"@tsrx/prettier-plugin": "0.3.
|
|
40
|
+
"@tsrx/core": "0.0.24",
|
|
41
|
+
"@tsrx/prettier-plugin": "0.3.44"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^24.3.0",
|
package/src/analyze.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
COMPONENT_DO_WHILE_STATEMENT_ERROR,
|
|
3
|
+
COMPONENT_FOR_IN_STATEMENT_ERROR,
|
|
4
|
+
COMPONENT_FOR_STATEMENT_ERROR,
|
|
5
|
+
COMPONENT_LOOP_BREAK_ERROR,
|
|
6
|
+
COMPONENT_LOOP_RETURN_ERROR,
|
|
7
|
+
COMPONENT_WHILE_STATEMENT_ERROR,
|
|
8
|
+
DIAGNOSTIC_CODES,
|
|
9
|
+
} from '@tsrx/core';
|
|
2
10
|
import { compile_tsrx } from './compile.js';
|
|
3
11
|
|
|
4
12
|
/**
|
|
@@ -28,6 +36,7 @@ function create_advice(input) {
|
|
|
28
36
|
/** @type {TSRXAdvice[]} */
|
|
29
37
|
const advice = [];
|
|
30
38
|
const error_codes = new Set(compileResult.errors.map((error) => error.code).filter(Boolean));
|
|
39
|
+
const error_messages = new Set(compileResult.errors.map((error) => error.message));
|
|
31
40
|
|
|
32
41
|
if (!compileResult.target) {
|
|
33
42
|
advice.push({
|
|
@@ -98,6 +107,36 @@ function create_advice(input) {
|
|
|
98
107
|
});
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
if (
|
|
111
|
+
error_messages.has(COMPONENT_LOOP_RETURN_ERROR) ||
|
|
112
|
+
error_messages.has(COMPONENT_LOOP_BREAK_ERROR)
|
|
113
|
+
) {
|
|
114
|
+
advice.push({
|
|
115
|
+
kind: 'component-loop-control-flow',
|
|
116
|
+
severity: 'error',
|
|
117
|
+
title: 'Use continue inside component for...of loops',
|
|
118
|
+
message:
|
|
119
|
+
'Top-level return and break statements are not valid inside a component for...of loop. Use continue to skip the current rendered item. Nested functions inside the loop keep ordinary JavaScript control flow.',
|
|
120
|
+
documentation: ['tsrx://docs/control-flow.md'],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
error_messages.has(COMPONENT_FOR_STATEMENT_ERROR) ||
|
|
126
|
+
error_messages.has(COMPONENT_FOR_IN_STATEMENT_ERROR) ||
|
|
127
|
+
error_messages.has(COMPONENT_WHILE_STATEMENT_ERROR) ||
|
|
128
|
+
error_messages.has(COMPONENT_DO_WHILE_STATEMENT_ERROR)
|
|
129
|
+
) {
|
|
130
|
+
advice.push({
|
|
131
|
+
kind: 'unsupported-component-loop',
|
|
132
|
+
severity: 'error',
|
|
133
|
+
title: 'Use for...of for component list rendering',
|
|
134
|
+
message:
|
|
135
|
+
'Component template scope supports for...of loops for rendering lists. Move regular for, for...in, while, and do...while loops into a nested function, event handler, effect, or helper.',
|
|
136
|
+
documentation: ['tsrx://docs/control-flow.md'],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
101
140
|
if (advice.length === 0 && compileResult.ok) {
|
|
102
141
|
advice.push({
|
|
103
142
|
kind: 'compile-clean',
|
package/src/authoring.js
ADDED
|
@@ -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
|
+
}
|
package/src/generated/docs.js
CHANGED
|
@@ -43,7 +43,7 @@ export const documentation_sections = [
|
|
|
43
43
|
use_cases:
|
|
44
44
|
'if else, for loops, switch, try catch, conditional rendering, lists, guard returns',
|
|
45
45
|
content:
|
|
46
|
-
'# Control Flow\n\nStandard JavaScript control flow can contain template statements inside component bodies and nested element children.\n\n```tsx\ncomponent List({ items }: { items: string[] }) {\n if (items.length === 0) {\n <p>"No items"</p>\n return;\n }\n\n <ul>\n for (const item of items) {\n <li>{item}</li>\n }\n </ul>\n}\n```\n\nA bare `return;` exits the current render path. A return with a value is invalid inside a TSRX component body.\n\nSource: website-tsrx/src/pages/features.tsrx#
|
|
46
|
+
'# Control Flow\n\nStandard JavaScript control flow can contain template statements inside component bodies and nested element children.\n\n```tsx\ncomponent List({ items }: { items: string[] }) {\n if (items.length === 0) {\n <p>"No items"</p>\n return;\n }\n\n <ul>\n for (const item of items; index i; key item) {\n if (!item) continue;\n <li>{item}</li>\n }\n </ul>\n}\n```\n\nA bare `return;` exits the current render path. A return with a value is invalid inside a TSRX component body.\n\nInside a component `for...of` loop, `continue` skips the current rendered iteration and is the only supported top-level loop control-flow statement. Top-level `return` and `break` are invalid inside component `for...of` loops; use `continue` for item skips, `return;` for non-loop guard exits, and `break` only for `switch` cases.\n\nComponent rendering supports `for...of` list loops. Regular `for`, `for...in`, `while`, and `do...while` loops are not supported in component template scope. Move imperative loops into a nested function, event handler, effect, or helper where normal JavaScript control flow rules apply.\n\nSource: website-tsrx/src/pages/features.tsrx#for',
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
slug: 'lazy-destructuring',
|
|
@@ -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
|
-
'
|
|
59
|
+
'style directive, scoped css, module server, submodule imports, compile-time identifiers',
|
|
60
60
|
content:
|
|
61
|
-
'# Style and Server Extensions\n\n
|
|
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,22 @@ 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
|
|
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.
|
|
310
|
+
5a. In component template scope, render lists with \`for...of\`; use \`continue\` to skip an item; do not use top-level \`return\` or \`break\` inside the loop, and do not use regular \`for\`, \`for...in\`, \`while\`, or \`do...while\` loops there.
|
|
281
311
|
${file_validation_step}
|
|
282
312
|
${compile_step}
|
|
283
|
-
|
|
284
|
-
9.
|
|
313
|
+
${authoring_step}
|
|
314
|
+
9. If \`compile-tsrx\` returns diagnostics, call \`analyze-tsrx\` for targeted authoring advice, fix the code, format it, and compile again.
|
|
315
|
+
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
316
|
}
|
|
286
317
|
|
|
287
318
|
/**
|
|
@@ -564,6 +595,87 @@ export function createTSRXMcpServer(options = {}) {
|
|
|
564
595
|
},
|
|
565
596
|
);
|
|
566
597
|
|
|
598
|
+
server.registerTool(
|
|
599
|
+
'review-tsrx-accessibility',
|
|
600
|
+
{
|
|
601
|
+
title: 'Review TSRX Accessibility',
|
|
602
|
+
description:
|
|
603
|
+
'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.',
|
|
604
|
+
inputSchema: {
|
|
605
|
+
code: z.string(),
|
|
606
|
+
filename: z.string().optional(),
|
|
607
|
+
target: TARGET_SCHEMA.optional(),
|
|
608
|
+
},
|
|
609
|
+
outputSchema: authoring_review_result_schema,
|
|
610
|
+
annotations: {
|
|
611
|
+
readOnlyHint: true,
|
|
612
|
+
destructiveHint: false,
|
|
613
|
+
openWorldHint: false,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
async (input) => {
|
|
617
|
+
const output = review_tsrx_accessibility_handler(input);
|
|
618
|
+
return {
|
|
619
|
+
content: [{ type: 'text', text: json_text(output) }],
|
|
620
|
+
structuredContent: output,
|
|
621
|
+
};
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
server.registerTool(
|
|
626
|
+
'review-tsrx-styles',
|
|
627
|
+
{
|
|
628
|
+
title: 'Review TSRX Styles',
|
|
629
|
+
description:
|
|
630
|
+
'Reviews TSRX source for style-authoring issues, including malformed style blocks, broad selectors, root styling that should live on explicit classes, and contrast risks.',
|
|
631
|
+
inputSchema: {
|
|
632
|
+
code: z.string(),
|
|
633
|
+
filename: z.string().optional(),
|
|
634
|
+
target: TARGET_SCHEMA.optional(),
|
|
635
|
+
},
|
|
636
|
+
outputSchema: authoring_review_result_schema,
|
|
637
|
+
annotations: {
|
|
638
|
+
readOnlyHint: true,
|
|
639
|
+
destructiveHint: false,
|
|
640
|
+
openWorldHint: false,
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
async (input) => {
|
|
644
|
+
const output = review_tsrx_styles_handler(input);
|
|
645
|
+
return {
|
|
646
|
+
content: [{ type: 'text', text: json_text(output) }],
|
|
647
|
+
structuredContent: output,
|
|
648
|
+
};
|
|
649
|
+
},
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
server.registerTool(
|
|
653
|
+
'review-tsrx-components',
|
|
654
|
+
{
|
|
655
|
+
title: 'Review TSRX Component Structure',
|
|
656
|
+
description:
|
|
657
|
+
'Reviews TSRX source for component decomposition opportunities when control flow, repeated item templates, or style blocks become large enough to hurt generated-code reliability.',
|
|
658
|
+
inputSchema: {
|
|
659
|
+
code: z.string(),
|
|
660
|
+
filename: z.string().optional(),
|
|
661
|
+
target: TARGET_SCHEMA.optional(),
|
|
662
|
+
},
|
|
663
|
+
outputSchema: authoring_review_result_schema,
|
|
664
|
+
annotations: {
|
|
665
|
+
readOnlyHint: true,
|
|
666
|
+
destructiveHint: false,
|
|
667
|
+
openWorldHint: false,
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
async (input) => {
|
|
671
|
+
const output = review_tsrx_components_handler(input);
|
|
672
|
+
return {
|
|
673
|
+
content: [{ type: 'text', text: json_text(output) }],
|
|
674
|
+
structuredContent: output,
|
|
675
|
+
};
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
|
|
567
679
|
if (!remote) {
|
|
568
680
|
server.registerTool(
|
|
569
681
|
'inspect-project',
|