@tsrx/mcp 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @tsrx/mcp
2
+
3
+ MCP server for TSRX language documentation and project context.
4
+
5
+ ## Usage
6
+
7
+ Run the server over stdio:
8
+
9
+ ```bash
10
+ npx -y @tsrx/mcp
11
+ ```
12
+
13
+ Generic MCP client config:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "tsrx": {
19
+ "command": "npx",
20
+ "args": ["-y", "@tsrx/mcp"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Hosted HTTP
27
+
28
+ Remote MCP clients need a hosted Streamable HTTP endpoint rather than a local
29
+ stdio command. This monorepo includes a deployment-neutral endpoint app in
30
+ `mcp-endpoint-website` that serves the same MCP server at `/mcp`.
31
+
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`.
35
+
36
+ Set `TSRX_MCP_BEARER_TOKEN` in the endpoint environment to require bearer-token
37
+ auth. Set `TSRX_MCP_CORS_ORIGIN` to restrict CORS for browser-based clients.
38
+
39
+ For local development in this monorepo, point at the source entrypoint:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "tsrx": {
45
+ "command": "node",
46
+ "args": ["/absolute/path/to/ripple/packages/tsrx-mcp/src/stdio.js"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Claude Desktop
53
+
54
+ Add the generic config above to `claude_desktop_config.json`.
55
+
56
+ ### Claude Code
57
+
58
+ ```bash
59
+ claude mcp add tsrx -- npx -y @tsrx/mcp
60
+ ```
61
+
62
+ For local development:
63
+
64
+ ```bash
65
+ claude mcp add tsrx-local -- node /absolute/path/to/ripple/packages/tsrx-mcp/src/stdio.js
66
+ ```
67
+
68
+ ### Cursor
69
+
70
+ Add the generic config above to your Cursor MCP settings.
71
+
72
+ ### Codex
73
+
74
+ Add the generic config above to your Codex MCP configuration.
75
+
76
+ ## Tools
77
+
78
+ - `list-sections` - list target-neutral TSRX documentation sections.
79
+ - `get-documentation` - fetch one or more TSRX documentation sections.
80
+ - `detect-target` - infer the active TSRX runtime target from project files.
81
+ - `inspect-project` - inspect target signals, TSRX packages, tooling, scripts, and
82
+ likely project commands.
83
+ - `compile-tsrx` - compile TSRX code with the inferred or explicit target compiler
84
+ and return diagnostics.
85
+ - `format-tsrx` - format TSRX code using the official Prettier plugin.
86
+ - `analyze-tsrx` - compile TSRX code and convert common diagnostics into
87
+ target-neutral authoring advice with linked docs resources.
88
+ - `validate-tsrx-file` - read a `.tsrx` file and run formatting, compilation, and
89
+ diagnostic advice in one read-only pass.
90
+
91
+ ## Agent Workflows
92
+
93
+ For an existing project, start with `inspect-project` to identify the TSRX target,
94
+ installed tooling, and likely validation commands. Use `detect-target` when only
95
+ the runtime target is needed.
96
+
97
+ For generated code, run `format-tsrx` first, then `compile-tsrx` with the inferred
98
+ or explicit target. If compilation fails, run `analyze-tsrx`, apply the advice,
99
+ format again, and compile again.
100
+
101
+ For an existing `.tsrx` file, prefer `validate-tsrx-file`. It reads the file and
102
+ runs formatting, compilation, and diagnostic advice in one read-only pass.
103
+
104
+ ## Resources
105
+
106
+ - `tsrx://docs/{slug}.md` - target-neutral TSRX documentation sections.
107
+ - `tsrx://targets/{target}.md` - handoff guidance for target-specific layers.
108
+
109
+ ## Prompts
110
+
111
+ - `tsrx-task` - target-aware workflow for TSRX coding tasks.
112
+
113
+ The core server stays target-neutral. Runtime-specific imports, bundler setup, and
114
+ framework semantics should live in target-specific skills, prompts, or resources
115
+ layered on top.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@tsrx/mcp",
3
+ "description": "MCP server for TSRX documentation and project context",
4
+ "license": "MIT",
5
+ "author": "Dominic Gannaway",
6
+ "version": "0.0.0",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
11
+ "directory": "packages/tsrx-mcp"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Ripple-TS/ripple/issues"
15
+ },
16
+ "homepage": "https://tsrx.dev",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./src/index.js",
23
+ "default": "./src/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "README.md",
28
+ "src"
29
+ ],
30
+ "bin": {
31
+ "tsrx-mcp": "./src/stdio.js"
32
+ },
33
+ "engines": {
34
+ "node": ">=22.0.0"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.29.0",
38
+ "prettier": "^3.8.3",
39
+ "zod": "^4.3.6",
40
+ "@tsrx/core": "0.0.19",
41
+ "@tsrx/prettier-plugin": "0.3.39"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^24.3.0",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.1.4"
47
+ },
48
+ "scripts": {
49
+ "generate:docs": "node scripts/generate-docs-index.js"
50
+ }
51
+ }
package/src/analyze.js ADDED
@@ -0,0 +1,195 @@
1
+ import { DIAGNOSTIC_CODES } from '@tsrx/core';
2
+ import { compile_tsrx } from './compile.js';
3
+
4
+ /**
5
+ * @typedef {{
6
+ * kind: string,
7
+ * severity: 'error' | 'warning' | 'info',
8
+ * title: string,
9
+ * message: string,
10
+ * documentation: string[],
11
+ * }} TSRXAdvice
12
+ */
13
+
14
+ /**
15
+ * @typedef {{
16
+ * ok: boolean,
17
+ * target: string | null,
18
+ * errors: Array<{ message: string, code?: string | null }>,
19
+ * }} TSRXCompileSummary
20
+ */
21
+
22
+ /**
23
+ * @param {{ compileResult: TSRXCompileSummary }} input
24
+ * @returns {TSRXAdvice[]}
25
+ */
26
+ function create_advice(input) {
27
+ const { compileResult } = input;
28
+ /** @type {TSRXAdvice[]} */
29
+ const advice = [];
30
+ const error_codes = new Set(compileResult.errors.map((error) => error.code).filter(Boolean));
31
+
32
+ if (!compileResult.target) {
33
+ advice.push({
34
+ kind: 'missing-target',
35
+ severity: 'error',
36
+ title: 'Select a TSRX runtime target',
37
+ message:
38
+ 'The compiler could not infer a runtime target. Call detect-target with a project cwd, or pass target as ripple, react, preact, solid, or vue.',
39
+ documentation: ['tsrx://docs/target-integration.md'],
40
+ });
41
+ }
42
+
43
+ const has_function_component_syntax = error_codes.has(DIAGNOSTIC_CODES.FUNCTION_COMPONENT_SYNTAX);
44
+ if (has_function_component_syntax) {
45
+ advice.push({
46
+ kind: 'function-component-syntax',
47
+ severity: 'warning',
48
+ title: 'Use TSRX component declarations',
49
+ message:
50
+ '.tsrx component files should use the component keyword. A React-style function returning JSX is usually the wrong authoring shape for TSRX.',
51
+ documentation: ['tsrx://docs/components.md'],
52
+ });
53
+ }
54
+
55
+ const fired_jsx_return = error_codes.has(DIAGNOSTIC_CODES.JSX_RETURN_IN_COMPONENT);
56
+
57
+ if (fired_jsx_return) {
58
+ advice.push({
59
+ kind: 'jsx-return-in-component',
60
+ severity: 'error',
61
+ title: 'Do not return JSX from component bodies',
62
+ message:
63
+ 'Inside a TSRX component body, template elements are statements. Replace `return <div />` with a template statement like `<div />`; use bare `return;` only for guard exits.',
64
+ documentation: ['tsrx://docs/components.md', 'tsrx://docs/tsx-expression-values.md'],
65
+ });
66
+ }
67
+
68
+ if (error_codes.has(DIAGNOSTIC_CODES.UNCLOSED_TAG)) {
69
+ advice.push({
70
+ kind: 'unclosed-tag',
71
+ severity: 'error',
72
+ title: 'Close template tags',
73
+ message:
74
+ 'The compiler found a template tag without a matching closing tag. Add the missing closing tag before changing target-specific code.',
75
+ documentation: ['tsrx://docs/components.md'],
76
+ });
77
+ }
78
+
79
+ if (error_codes.has(DIAGNOSTIC_CODES.MISMATCHED_CLOSING_TAG)) {
80
+ advice.push({
81
+ kind: 'mismatched-closing-tag',
82
+ severity: 'error',
83
+ title: 'Match closing tags',
84
+ message:
85
+ 'The compiler found a closing tag that does not match the current open template tag. Align the tag names or close the inner tag first.',
86
+ documentation: ['tsrx://docs/components.md'],
87
+ });
88
+ }
89
+
90
+ if (error_codes.has(DIAGNOSTIC_CODES.JSX_EXPRESSION_VALUE)) {
91
+ advice.push({
92
+ kind: 'jsx-expression-value',
93
+ severity: 'info',
94
+ title: 'Wrap expression-position JSX',
95
+ message:
96
+ 'When JSX is needed as a value, wrap it in a fragment `<>...</>` or `<tsx>...</tsx>` so TSRX knows it is an expression rather than a template statement.',
97
+ documentation: ['tsrx://docs/tsx-expression-values.md'],
98
+ });
99
+ }
100
+
101
+ if (advice.length === 0 && compileResult.ok) {
102
+ advice.push({
103
+ kind: 'compile-clean',
104
+ severity: 'info',
105
+ title: 'TSRX compiled successfully',
106
+ message:
107
+ 'No target-neutral TSRX issues were found. For runtime API or bundler details, continue with the target-specific guidance for the detected target.',
108
+ documentation: ['tsrx://docs/target-integration.md'],
109
+ });
110
+ }
111
+
112
+ if (advice.length === 0) {
113
+ advice.push({
114
+ kind: 'compiler-diagnostic',
115
+ severity: 'error',
116
+ title: 'Compiler diagnostics need review',
117
+ message:
118
+ 'The TSRX compiler returned diagnostics that do not match a known MCP hint yet. Use the normalized compiler errors and relevant docs sections to revise the source.',
119
+ documentation: ['tsrx://docs/overview.md'],
120
+ });
121
+ }
122
+
123
+ return advice;
124
+ }
125
+
126
+ /**
127
+ * @param {{
128
+ * code: string,
129
+ * compileResult: {
130
+ * ok: boolean,
131
+ * target: string | null,
132
+ * compilerPackage: string | null,
133
+ * filename: string,
134
+ * cwd: string,
135
+ * errors: Array<{
136
+ * message: string,
137
+ * code: string | null,
138
+ * type: string | null,
139
+ * fileName: string | null,
140
+ * pos: number | null,
141
+ * end: number | null,
142
+ * raisedAt: number | null,
143
+ * loc: unknown
144
+ * }>
145
+ * }
146
+ * }} input
147
+ */
148
+ export function analyze_tsrx_result(input) {
149
+ const { compileResult } = input;
150
+ const advice = create_advice({
151
+ compileResult,
152
+ });
153
+
154
+ return {
155
+ ok: compileResult.ok,
156
+ target: compileResult.target,
157
+ compilerPackage: compileResult.compilerPackage,
158
+ filename: compileResult.filename,
159
+ cwd: compileResult.cwd,
160
+ errors: compileResult.errors,
161
+ advice,
162
+ nextSteps: compileResult.ok
163
+ ? [
164
+ 'Use target-specific resources for runtime semantics.',
165
+ 'Compile again after changing generated TSRX.',
166
+ ]
167
+ : [
168
+ 'Apply the highest-severity advice first.',
169
+ 'Fetch the linked docs resources for syntax details.',
170
+ 'Run compile-tsrx again after revising the source.',
171
+ ],
172
+ };
173
+ }
174
+
175
+ /**
176
+ * @param {{
177
+ * code: string,
178
+ * filename?: string,
179
+ * target?: string,
180
+ * cwd?: string,
181
+ * collect?: boolean,
182
+ * loose?: boolean,
183
+ * mode?: 'client' | 'server'
184
+ * }} input
185
+ */
186
+ export async function analyze_tsrx(input) {
187
+ const compileResult = await compile_tsrx({
188
+ ...input,
189
+ includeCode: false,
190
+ });
191
+ return analyze_tsrx_result({
192
+ code: input.code,
193
+ compileResult,
194
+ });
195
+ }
package/src/compile.js ADDED
@@ -0,0 +1,235 @@
1
+ import path from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { detect_target, TARGET_CANDIDATES } from './target.js';
5
+
6
+ const VALID_TARGETS = new Set(TARGET_CANDIDATES.map((candidate) => candidate.target));
7
+
8
+ /**
9
+ * @typedef {{
10
+ * message: string,
11
+ * code: string | null,
12
+ * type: string | null,
13
+ * fileName: string | null,
14
+ * pos: number | null,
15
+ * end: number | null,
16
+ * raisedAt: number | null,
17
+ * loc: unknown
18
+ * }} NormalizedCompileError
19
+ */
20
+
21
+ /**
22
+ * @param {string | null | undefined} target
23
+ */
24
+ function get_target_candidate(target) {
25
+ if (!target) return null;
26
+ return TARGET_CANDIDATES.find((candidate) => candidate.target === target) ?? null;
27
+ }
28
+
29
+ /**
30
+ * @param {unknown} error
31
+ * @param {string} filename
32
+ * @returns {NormalizedCompileError}
33
+ */
34
+ function normalize_error(error, filename) {
35
+ if (error && typeof error === 'object') {
36
+ const candidate = /** @type {Record<string, unknown>} */ (error);
37
+ return {
38
+ message: candidate.message ? String(candidate.message) : String(error),
39
+ code: candidate.code ? String(candidate.code) : null,
40
+ type: candidate.type ? String(candidate.type) : null,
41
+ fileName: candidate.fileName ? String(candidate.fileName) : filename,
42
+ pos: typeof candidate.pos === 'number' ? candidate.pos : null,
43
+ end: typeof candidate.end === 'number' ? candidate.end : null,
44
+ raisedAt: typeof candidate.raisedAt === 'number' ? candidate.raisedAt : null,
45
+ loc: candidate.loc ?? null,
46
+ };
47
+ }
48
+ return {
49
+ message: String(error),
50
+ code: null,
51
+ type: null,
52
+ fileName: filename,
53
+ pos: null,
54
+ end: null,
55
+ raisedAt: null,
56
+ loc: null,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * @param {unknown} errors
62
+ * @param {string} filename
63
+ */
64
+ function normalize_errors(errors, filename) {
65
+ return Array.isArray(errors) ? errors.map((error) => normalize_error(error, filename)) : [];
66
+ }
67
+
68
+ /**
69
+ * @param {string} cwd
70
+ * @param {string | null} package_json_path
71
+ */
72
+ function create_project_require(cwd, package_json_path) {
73
+ return createRequire(package_json_path ?? path.join(path.resolve(cwd), 'package.json'));
74
+ }
75
+
76
+ /**
77
+ * @param {string} compiler_package
78
+ * @param {string} cwd
79
+ * @param {string | null} package_json_path
80
+ */
81
+ async function import_compiler(compiler_package, cwd, package_json_path) {
82
+ const project_require = create_project_require(cwd, package_json_path);
83
+ const resolved = project_require.resolve(compiler_package);
84
+ return import(pathToFileURL(resolved).href);
85
+ }
86
+
87
+ /**
88
+ * @param {unknown} result
89
+ */
90
+ function get_generated_code(result) {
91
+ if (!result || typeof result !== 'object') return null;
92
+ const output = /** @type {Record<string, unknown>} */ (result);
93
+ if (typeof output.code === 'string') return output.code;
94
+ if (
95
+ output.js &&
96
+ typeof output.js === 'object' &&
97
+ typeof (/** @type {Record<string, unknown>} */ (output.js).code) === 'string'
98
+ ) {
99
+ return /** @type {string} */ (/** @type {Record<string, unknown>} */ (output.js).code);
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * @param {unknown} result
106
+ */
107
+ function get_generated_css(result) {
108
+ if (!result || typeof result !== 'object') return null;
109
+ const output = /** @type {Record<string, unknown>} */ (result);
110
+ if (
111
+ output.css &&
112
+ typeof output.css === 'object' &&
113
+ typeof (/** @type {Record<string, unknown>} */ (output.css).code) === 'string'
114
+ ) {
115
+ return /** @type {string} */ (/** @type {Record<string, unknown>} */ (output.css).code);
116
+ }
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * @param {{
122
+ * code: string,
123
+ * filename?: string,
124
+ * target?: string,
125
+ * cwd?: string,
126
+ * collect?: boolean,
127
+ * loose?: boolean,
128
+ * includeCode?: boolean,
129
+ * mode?: 'client' | 'server'
130
+ * }} input
131
+ */
132
+ export async function compile_tsrx(input) {
133
+ const filename = input.filename ?? 'Component.tsrx';
134
+ const detection = detect_target(input.cwd);
135
+ const cwd = detection.cwd;
136
+ const target = input.target ?? detection.detectedTarget;
137
+
138
+ if (!target) {
139
+ return {
140
+ ok: false,
141
+ target: null,
142
+ compilerPackage: null,
143
+ filename,
144
+ cwd,
145
+ errors: [
146
+ {
147
+ message:
148
+ detection.confidence === 'ambiguous'
149
+ ? detection.message
150
+ : `Could not infer a TSRX target. Pass target explicitly. ${detection.message}`,
151
+ code: null,
152
+ type: null,
153
+ fileName: filename,
154
+ pos: null,
155
+ end: null,
156
+ raisedAt: null,
157
+ loc: null,
158
+ },
159
+ ],
160
+ code: null,
161
+ css: null,
162
+ };
163
+ }
164
+
165
+ if (!VALID_TARGETS.has(target)) {
166
+ return {
167
+ ok: false,
168
+ target,
169
+ compilerPackage: null,
170
+ filename,
171
+ cwd,
172
+ errors: [
173
+ {
174
+ message: `Unknown TSRX target "${target}".`,
175
+ code: null,
176
+ type: null,
177
+ fileName: filename,
178
+ pos: null,
179
+ end: null,
180
+ raisedAt: null,
181
+ loc: null,
182
+ },
183
+ ],
184
+ code: null,
185
+ css: null,
186
+ };
187
+ }
188
+
189
+ const candidate = get_target_candidate(target);
190
+ if (!candidate) {
191
+ throw new Error(`Missing compiler candidate for target "${target}".`);
192
+ }
193
+
194
+ try {
195
+ const compiler = await import_compiler(
196
+ candidate.compilerPackage,
197
+ cwd,
198
+ detection.packageJsonPath,
199
+ );
200
+ if (typeof compiler.compile !== 'function') {
201
+ throw new Error(`${candidate.compilerPackage} does not export a compile() function.`);
202
+ }
203
+
204
+ const result = compiler.compile(input.code, filename, {
205
+ collect: input.collect ?? true,
206
+ loose: input.loose,
207
+ mode: input.mode,
208
+ });
209
+ const errors = normalize_errors(result?.errors, filename);
210
+ const code = get_generated_code(result);
211
+ const css = get_generated_css(result);
212
+
213
+ return {
214
+ ok: errors.length === 0,
215
+ target,
216
+ compilerPackage: candidate.compilerPackage,
217
+ filename,
218
+ cwd,
219
+ errors,
220
+ code: input.includeCode ? code : null,
221
+ css: input.includeCode ? css : null,
222
+ };
223
+ } catch (error) {
224
+ return {
225
+ ok: false,
226
+ target,
227
+ compilerPackage: candidate.compilerPackage,
228
+ filename,
229
+ cwd,
230
+ errors: [normalize_error(error, filename)],
231
+ code: null,
232
+ css: null,
233
+ };
234
+ }
235
+ }
package/src/docs.js ADDED
@@ -0,0 +1,50 @@
1
+ export { documentation_sections } from './generated/docs.js';
2
+
3
+ import { documentation_sections } from './generated/docs.js';
4
+
5
+ /** @typedef {{ slug: string, title: string, use_cases: string, content: string }} DocumentationSection */
6
+
7
+ /**
8
+ * @param {string} value
9
+ */
10
+ function normalize(value) {
11
+ return value
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9]+/g, '-')
14
+ .replace(/^-|-$/g, '');
15
+ }
16
+
17
+ /**
18
+ * @returns {DocumentationSection[]}
19
+ */
20
+ export function list_documentation_sections() {
21
+ return documentation_sections;
22
+ }
23
+
24
+ /**
25
+ * @param {string} section
26
+ */
27
+ export function find_documentation_section(section) {
28
+ const normalized = normalize(section);
29
+ return (
30
+ documentation_sections.find(
31
+ (candidate) =>
32
+ candidate.slug === normalized ||
33
+ normalize(candidate.title) === normalized ||
34
+ candidate.slug === section,
35
+ ) ?? null
36
+ );
37
+ }
38
+
39
+ /**
40
+ * @param {string} section
41
+ */
42
+ export function find_similar_documentation_sections(section) {
43
+ const normalized = normalize(section);
44
+ return documentation_sections.filter(
45
+ (candidate) =>
46
+ candidate.slug.includes(normalized) ||
47
+ normalize(candidate.title).includes(normalized) ||
48
+ candidate.use_cases.toLowerCase().includes(section.toLowerCase()),
49
+ );
50
+ }