davanstrien HF Staff Claude commited on
Commit
d0808a4
·
1 Parent(s): c765308

Deploy Liftoscript MCP validator

Browse files

- Minimal Lezer-based validator for Liftoscript
- Supports both planner and expression validation
- MCP server endpoint for Claude integration
- Includes JavaScript parsers and Gradio interface

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (8) hide show
  1. README.md +51 -4
  2. app.py +301 -0
  3. liftoscript-parser.js +27 -0
  4. minimal-validator.js +161 -0
  5. package.json +16 -0
  6. packages.txt +3 -0
  7. planner-parser.js +27 -0
  8. requirements.txt +2 -0
README.md CHANGED
@@ -1,12 +1,59 @@
1
  ---
2
- title: Liftosaur Mcp
3
- emoji:
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.35.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Liftoscript MCP Validator
3
+ emoji: 💪
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.0.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Liftoscript MCP Validator
13
+
14
+ A Model Context Protocol (MCP) server that validates [Liftoscript](https://www.liftosaur.com/docs/docs/liftoscript) workout programs.
15
+
16
+ ## Features
17
+
18
+ - ✅ Validates both **Planner syntax** (Week/Day format) and **Liftoscript expressions**
19
+ - 🚀 Lightweight Lezer-based parser
20
+ - 🤖 MCP integration for Claude Desktop
21
+ - 📍 Detailed error messages with line/column information
22
+
23
+ ## MCP Configuration
24
+
25
+ Add this to your Claude Desktop config:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "liftoscript": {
31
+ "command": "npx",
32
+ "args": ["mcp-remote", "https://davanstrien-liftosaur-mcp.hf.space/gradio_api/mcp/sse"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Example Usage
39
+
40
+ ### Planner Format
41
+ ```
42
+ # Week 1
43
+ ## Day 1
44
+ Squat / 3x5 / progress: lp(5lb)
45
+ Bench Press / 3x8
46
+ ```
47
+
48
+ ### Liftoscript Expression
49
+ ```javascript
50
+ if (completedReps >= reps) {
51
+ state.weight += 5lb
52
+ }
53
+ ```
54
+
55
+ ## Resources
56
+
57
+ - [Liftoscript Documentation](https://www.liftosaur.com/docs/docs/liftoscript)
58
+ - [Liftosaur App](https://www.liftosaur.com/)
59
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
app.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import subprocess
3
+ import json
4
+ import os
5
+ from typing import Dict, Any
6
+
7
+ def setup_parser():
8
+ """Setup the minimal parser - just needs npm install."""
9
+ try:
10
+ # Check if Node.js is available
11
+ result = subprocess.run(['node', '--version'], capture_output=True, text=True)
12
+ if result.returncode != 0:
13
+ return False, "Node.js not found. Please ensure Node.js is installed."
14
+
15
+ # Check if dependencies are installed
16
+ if not os.path.exists('node_modules/@lezer/lr'):
17
+ print("Installing dependencies...")
18
+ install_result = subprocess.run(
19
+ ['npm', 'install'],
20
+ capture_output=True,
21
+ text=True,
22
+ timeout=60
23
+ )
24
+ if install_result.returncode != 0:
25
+ return False, f"Failed to install dependencies: {install_result.stderr}"
26
+
27
+ # Check if parser files exist
28
+ required_files = ['minimal-validator.js', 'liftoscript-parser.js', 'planner-parser.js']
29
+ missing_files = [f for f in required_files if not os.path.exists(f)]
30
+
31
+ if missing_files:
32
+ return False, f"Missing required files: {', '.join(missing_files)}"
33
+
34
+ # Test the validator
35
+ test_result = subprocess.run(
36
+ ['node', 'minimal-validator.js', 'state.weight = 100lb', '--json'],
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=5
40
+ )
41
+
42
+ if test_result.returncode != 0:
43
+ return False, "Validator test failed"
44
+
45
+ return True, "Parser ready (minimal Lezer-based validator)"
46
+
47
+ except Exception as e:
48
+ return False, f"Setup error: {str(e)}"
49
+
50
+ def validate_liftoscript(script: str) -> Dict[str, Any]:
51
+ """
52
+ Validate a Liftoscript program using the minimal Lezer-based parser.
53
+
54
+ This tool validates Liftoscript workout programs and returns detailed error information
55
+ if the syntax is invalid. It supports both planner format (Week/Day/Exercise) and
56
+ pure Liftoscript expressions.
57
+
58
+ Args:
59
+ script: The Liftoscript code to validate (either planner format or expression)
60
+
61
+ Returns:
62
+ Validation result dictionary with keys:
63
+ - valid (bool): Whether the script is syntactically valid
64
+ - error (str|None): Error message if invalid
65
+ - line (int|None): Line number of error
66
+ - column (int|None): Column number of error
67
+ - type (str|None): Detected script type ('planner' or 'liftoscript')
68
+ """
69
+ if not script or not script.strip():
70
+ return {
71
+ "valid": False,
72
+ "error": "Empty script provided",
73
+ "line": None,
74
+ "column": None,
75
+ "type": None
76
+ }
77
+
78
+ try:
79
+ # Use the minimal parser wrapper
80
+ wrapper = 'parser-wrapper-minimal.js' if os.path.exists('parser-wrapper-minimal.js') else 'minimal-validator.js'
81
+
82
+ # Call the validator
83
+ result = subprocess.run(
84
+ ['node', wrapper, '-'] if wrapper == 'parser-wrapper-minimal.js' else ['node', wrapper, '-', '--json'],
85
+ input=script,
86
+ capture_output=True,
87
+ text=True,
88
+ timeout=10
89
+ )
90
+
91
+ if result.returncode != 0 and not result.stdout:
92
+ return {
93
+ "valid": False,
94
+ "error": f"Validator error: {result.stderr}",
95
+ "line": None,
96
+ "column": None,
97
+ "type": None
98
+ }
99
+
100
+ # Parse JSON response
101
+ try:
102
+ validation_result = json.loads(result.stdout)
103
+
104
+ # Handle both formats (wrapper and direct)
105
+ if 'errors' in validation_result:
106
+ # Wrapper format
107
+ if validation_result.get('errors') and len(validation_result['errors']) > 0:
108
+ first_error = validation_result['errors'][0]
109
+ return {
110
+ "valid": False,
111
+ "error": first_error.get('message', 'Unknown error'),
112
+ "line": first_error.get('line'),
113
+ "column": first_error.get('column'),
114
+ "type": validation_result.get('type')
115
+ }
116
+ else:
117
+ return {
118
+ "valid": True,
119
+ "error": None,
120
+ "line": None,
121
+ "column": None,
122
+ "type": validation_result.get('type', 'unknown')
123
+ }
124
+ else:
125
+ # Direct format
126
+ if validation_result.get('valid'):
127
+ return {
128
+ "valid": True,
129
+ "error": None,
130
+ "line": None,
131
+ "column": None,
132
+ "type": validation_result.get('type', 'unknown')
133
+ }
134
+ else:
135
+ error = validation_result.get('error', {})
136
+ return {
137
+ "valid": False,
138
+ "error": error.get('message', 'Unknown error'),
139
+ "line": error.get('line'),
140
+ "column": error.get('column'),
141
+ "type": error.get('type')
142
+ }
143
+
144
+ except json.JSONDecodeError:
145
+ return {
146
+ "valid": False,
147
+ "error": "Invalid parser response",
148
+ "line": None,
149
+ "column": None,
150
+ "type": None
151
+ }
152
+
153
+ except subprocess.TimeoutExpired:
154
+ return {
155
+ "valid": False,
156
+ "error": "Parser timeout - script too complex",
157
+ "line": None,
158
+ "column": None,
159
+ "type": None
160
+ }
161
+ except Exception as e:
162
+ return {
163
+ "valid": False,
164
+ "error": f"Validation error: {str(e)}",
165
+ "line": None,
166
+ "column": None,
167
+ "type": None
168
+ }
169
+
170
+ # Create Gradio interface
171
+ with gr.Blocks(title="Liftoscript Validator MCP Server") as app:
172
+ gr.Markdown("""
173
+ # 💪 Liftoscript Validator MCP Server
174
+
175
+ This tool validates [Liftoscript](https://www.liftosaur.com/docs/docs/liftoscript) workout programs using a **minimal Lezer-based parser** extracted from the Liftosaur repository.
176
+
177
+ ## 🚀 Lightweight Implementation
178
+
179
+ This proof-of-concept uses only:
180
+ - The compiled Lezer parsers (no grammar compilation needed)
181
+ - Basic syntax validation (no semantic analysis)
182
+ - Zero dependencies on the full Liftosaur codebase
183
+
184
+ ## ✅ Features:
185
+ - Validates both **Planner syntax** (Week/Day format) and **Liftoscript expressions**
186
+ - Automatic detection of script type
187
+ - Detailed error messages with line/column information
188
+ - Lightweight and fast (no heavy builds required)
189
+
190
+ ## 📦 MCP Server Configuration
191
+
192
+ Add to your `claude_desktop_config.json`:
193
+ ```json
194
+ {
195
+ "mcpServers": {
196
+ "liftoscript": {
197
+ "command": "npx",
198
+ "args": ["mcp-remote", "https://davanstrien-liftosaur-mcp.hf.space/gradio_api/mcp/sse"]
199
+ }
200
+ }
201
+ }
202
+ ```
203
+ """)
204
+
205
+ # Check parser setup
206
+ parser_ready, setup_message = setup_parser()
207
+
208
+ with gr.Row():
209
+ with gr.Column():
210
+ if parser_ready:
211
+ gr.Markdown(f"**✅ Parser Status**: {setup_message}")
212
+ else:
213
+ gr.Markdown(f"**⚠️ Parser Status**: {setup_message}")
214
+
215
+ with gr.Row():
216
+ with gr.Column():
217
+ script_input = gr.Textbox(
218
+ label="Liftoscript Code",
219
+ placeholder="""Enter either Planner format:
220
+
221
+ # Week 1
222
+ ## Day 1
223
+ Squat / 3x5 / progress: lp(5lb)
224
+ Bench Press / 3x8 @8
225
+
226
+ Or pure Liftoscript expressions:
227
+
228
+ if (completedReps >= reps) {
229
+ state.weight = state.weight + 5lb
230
+ }""",
231
+ lines=15
232
+ )
233
+ validate_btn = gr.Button("Validate", variant="primary", interactive=parser_ready)
234
+
235
+ with gr.Column():
236
+ validation_output = gr.JSON(label="Validation Result")
237
+
238
+ gr.Markdown("""
239
+ ### 📝 Example Scripts
240
+
241
+ Click any example below to test:
242
+ """)
243
+
244
+ gr.Examples(
245
+ examples=[
246
+ # Planner examples
247
+ ["# Week 1\n## Day 1\nSquat / 3x5 / progress: lp(5lb)\nBench Press / 3x8"],
248
+ ["Deadlift / 1x5 / warmup: 1x5 135lb, 1x3 225lb, 1x1 315lb"],
249
+ ["Bench Press / 3x8-12 @8 / progress: dp"],
250
+
251
+ # Liftoscript examples
252
+ ["state.weight = 100lb"],
253
+ ["if (completedReps >= reps) { state.weight += 5lb }"],
254
+ ["for (r in completedReps) {\n if (r < reps[index]) {\n state.weight -= 5%\n }\n}"],
255
+
256
+ # Invalid examples (for testing)
257
+ ["// Missing closing brace\nif (true) { state.weight = 100lb"],
258
+ ["// Invalid format\nSquat 3x5"],
259
+ ],
260
+ inputs=script_input,
261
+ label="Example Scripts"
262
+ )
263
+
264
+ validate_btn.click(
265
+ fn=validate_liftoscript,
266
+ inputs=script_input,
267
+ outputs=validation_output
268
+ )
269
+
270
+ gr.Markdown("""
271
+ ---
272
+
273
+ ## 🎯 Value Proposition
274
+
275
+ This minimal validator demonstrates that:
276
+ 1. **Syntax validation** can be separated from full program evaluation
277
+ 2. The Lezer parsers can be used **independently**
278
+ 3. A standalone parser package would enable many tools and integrations
279
+
280
+ ## 🤝 For Liftosaur Maintainers
281
+
282
+ This PoC shows demand for a standalone parser package. Benefits:
283
+ - Enable third-party tools (editors, linters, formatters)
284
+ - Support AI assistants like Claude via MCP
285
+ - Grow the Liftoscript ecosystem
286
+ - Minimal maintenance burden (just the parser, not the full runtime)
287
+
288
+ ## 📚 Resources
289
+ - [Liftoscript Documentation](https://www.liftosaur.com/docs/docs/liftoscript)
290
+ - [Liftosaur App](https://www.liftosaur.com/)
291
+ - [GitHub Repository](https://github.com/astashov/liftosaur)
292
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
293
+ """)
294
+
295
+ # Launch with MCP server enabled
296
+ if __name__ == "__main__":
297
+ app.launch(
298
+ mcp_server=True,
299
+ server_name="0.0.0.0",
300
+ share=False
301
+ )
liftoscript-parser.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Liftoscript parser - compiled from Lezer grammar
2
+ // This is a standalone version extracted from Liftosaur
3
+ const { LRParser } = require("@lezer/lr");
4
+
5
+ const spec_Keyword = { __proto__: null, if: 100, else: 102, for: 104 };
6
+ const parser = LRParser.deserialize({
7
+ version: 14,
8
+ states:
9
+ ",fQcQPOOO!iQPO'#C}OOQO'#Cd'#CdO#cQPO'#CdO${QPO'#DXOcQPO'#CiO%SQPO'#CjO%ZQPO'#ClO%`QPO'#CnO%eQQO'#CrO%mQPO'#CuO'ZQPO'#DXOcQPO'#C{OOQO'#DX'#DXQcQPOOOcQPO,58yOcQPO,58yOcQPO,58yOcQPO,58yOcQPO,59VOOQO,59O,59OOOQO,59Q,59QO'eQPO,59TOOQO,59U,59UO'lQPO,59UO'sQPO,59WO'xQPO,59YO'}QPO,59^OcQPO,59^O(SQSO,59aO(ZQPO,59fOcQPO,59]OcQPO,59dOOQO,59g,59gOOQO-E6{-E6{O)uQPO1G.eOOQO1G.e1G.eO)|QPO1G.eO*TQPO1G.eO+hQPO1G.qOOQO1G.o1G.oOOQO1G.p1G.pO+oQPO1G.rO-VQQO1G.tOOQO1G.x1G.xO-[QPO'#CtO-cQPO1G.xO-hQPO'#CvOOQO'#Cw'#CwOOQO'#Cv'#CvO-rQPO1G.{O-zQPO1G/QOcQPO'#DQO.UQPO1G/QOOQO1G/Q1G/QO.^QPO1G.wO/dQPO1G/OOcQPO7+$]O0jQPO7+$^O0rQPO7+$^OcQPO7+$`O2YQQO7+$dO(SQSO'#DPO2_QPO7+$gOOQO7+$g7+$gO2gQPO7+$lOOQO7+$l7+$lO2oQPO,59lOOQO-E7O-E7OO2yQPO<<GwO%ZQPO,59jOOQO<<Gx<<GxO0jQPO<<GxOOQO-E6|-E6|O4PQPO'#CpO4WQPO<<GzO4]QPO<<HOOOQO,59k,59kOOQO-E6}-E6}OOQO<<HR<<HROOQO<<HW<<HWO'sQPO1G/UOOQOAN=dAN=dP0mQPO'#DOO'sQPOAN=fOOQOAN=jAN=jOOQO7+$p7+$pOOQOG23QG23Q",
10
+ stateData:
11
+ "4m~OwOSPOSxOSyOSzOS~OSROXQO[]OaYOcZOgXOp[O|TO!OUO!SVO!UWO~OS_OT`OUaOVbO!QcO~OXqX[qXaqXcqXgqXpqXuqX|qX!OqX!SqX!UqX!PqX~P!WOXdO~OS{XT{XU{XV{XX{X[{Xa{Xc{Xg{Xp{Xu{X|{X!O{X!Q{X!S{X!U{X}{X!P{X!R{X!X{X!]{X~OZeO~P#hO!PgO~PcO|TO~O|jO~O!WlO!YkO~O|nO!WmOSiXTiXUiXViXXiX[iXaiXciXgiXmiXpiXuiX!OiX!QiX!SiX!UiX![iX}iX!PiX!RiX!XiX!]iX~OmpO![oO~P#hO}xO~P!WO!PyO~PcO!OUO~Oc{O~Oa|O~O!Z!QO~PcO}!WO!]!UO~PcOT`OURiVRiXRi[RiaRicRigRipRiuRi|Ri!ORi!QRi!SRi!URi}Ri!PRi!RRi!XRi!]Ri~OSRi~P(eOS_O~P(eOS_OT`OUaOVRiXRi[RiaRicRigRipRiuRi|Ri!ORi!QRi!SRi!URi}Ri!PRi!RRi!XRi!]Ri~O!R!ZO~P!WO!T![OS`iT`iU`iV`iX`i[`ia`ic`ig`ip`iu`i|`i!O`i!Q`i!S`i!U`i}`i!P`i!R`i!X`i!]`i~O!V!^O~O!XhX~P!WO!X!_O~O!RjX!XjX~P!WO!R!`O!X!bO~O}!dO!]!UO~P!WO}!dO!]!UO~OXei[eiaeiceigeipeiuei|ei!Oei!Sei!Uei}ei!Pei!Rei!Xei!]ei~P!WOXli[lialicliglipliuli|li!Oli!Sli!Uli}li!Pli!Rli!Xli!]li~P!WO!OUO!S!hO~O!T!jOS`qT`qU`qV`qX`q[`qa`qc`qg`qp`qu`q|`q!O`q!Q`q!S`q!U`q}`q!P`q!R`q!X`q!]`q~O!Y!nO~O!R!`O!X!qO~O}!rO!]!UO~O}ta!]ta~P!WOX_y[_ya_yc_yg_yp_yu_y|_y!O_y!S_y!U_y}_y!P_y!R_y!X_y!]_y~P!WO}dX~P!WO}!vO~Oa!wO~OPTg[XScZaZ~",
12
+ goto:
13
+ "'O|PPP}PPPP!eP}PP!{#i}}P}P$`}$cP$y$c$|%S}P}}P%W%b%h%nPPPPPP%xy]OTU[^_`abchlmnop!U!Z!^!`ySOTU[^_`abchlmnop!U!Z!^!`x]OTU[^_`abchlmnop!U!Z!^!`QiVR!s!hx]OTU[^_`abchlmnop!U!Z!^!`QziQ!i![Q!t!jQ!x!sR!y!vR!m!^yZOTU[^_`abchlmnop!U!Z!^!`R!OlQ!SmR!o!`T!Rm!`Q^OQhUTr^hQ!]zR!k!]Q!a!SR!p!aQ!VnQ!c!TT!f!V!cWPOU^hQfTQq[Qs_Qt`QuaQvbQwcQ}lS!Pm!`Q!TnQ!XoQ!YpQ!e!UQ!g!ZR!l!^",
14
+ nodeNames:
15
+ "⚠ LineComment Program BinaryExpression Plus Times Cmp AndOr NumberExpression Number WeightExpression Unit Percentage ParenthesisExpression BlockExpression Ternary IfExpression Keyword ForExpression Variable ForInExpression AssignmentExpression StateVariable StateKeyword StateVariableIndex VariableExpression VariableIndex Wildcard IncAssignmentExpression IncAssignment BuiltinFunctionExpression UnaryExpression Not",
16
+ maxTerm: 59,
17
+ skippedNodes: [0, 1],
18
+ repeatNodeCount: 4,
19
+ tokenData:
20
+ "1S~R}X^$Opq$Oqr$suv%Qvw%Vxy%byz%gz{%l{|%{|}&T}!O%{!O!P&Y!P!Q&r!Q!['f![!](e!]!^(j!^!_(o!_!`(w!`!a(o!a!b)P!c!})U!}#O)g#P#Q)l#T#])U#]#^)q#^#_)U#_#`*m#`#a+i#a#g)U#g#h,Q#h#j)U#j#k.b#k#o)U#o#p0_#p#q0l#q#r0r#r#s0w#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$TYw~X^$Opq$O#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$xPp~!_!`${~%QOU~P%VOTP~%YPvw%]~%bOV~~%gO|~~%lO}~T%sP!ZSTP!_!`%vP%{OmP~&QPS~!_!`%v~&YO!]~V&_P!YQ!Q![&bT&gQXTuv&m!Q![&bT&rO[T~&wQTP!P!Q&}!_!`%v~'SSP~OY&}Z;'S&};'S;=`'`<%lO&}~'cP;=`<%l&}T'kRXTuv&m!O!P't!Q!['fT'yQXTuv&m!Q![(PT(URXTuv&m!O!P(_!Q![(PT(bP!Q![(P~(jO!R~~(oOx~~(tPU~!_!`${~(|P![~!_!`${~)UO!Q~T)ZSaT!Q![)U!c!})U#R#S)U#T#o)U~)lO!W~~)qO!X~V)vUaT!Q![)U!c!})U#R#S)U#T#b)U#b#c*Y#c#o)UV*aS!VQaT!Q![)U!c!})U#R#S)U#T#o)U~*rUaT!Q![)U!c!})U#R#S)U#T#Z)U#Z#[+U#[#o)U~+]SZ~aT!Q![)U!c!})U#R#S)U#T#o)U~+nUaT!Q![)U!c!})U#R#S)U#T#U)U#U#V+U#V#o)U~,VUaT!Q![)U!c!})U#R#S)U#T#h)U#h#i,i#i#o)U~,nTaT!Q![)U!c!})U#R#S)U#T#U,}#U#o)U~-SUaT!Q![)U!c!})U#R#S)U#T#h)U#h#i-f#i#o)U~-kUaT!Q![)U!c!})U#R#S)U#T#X)U#X#Y-}#Y#o)U~.USg~aT!Q![)U!c!})U#R#S)U#T#o)U~.gTaT!Q![)U!c!})U#R#S)U#T#U.v#U#o)U~.{UaT!Q![)U!c!})U#R#S)U#T#f)U#f#g/_#g#o)U~/dTaT!O!P/s!Q![)U!c!})U#R#S)U#T#o)U~/vQ!c!}/|#T#o/|~0RSc~!Q![/|!c!}/|#R#S/|#T#o/|~0dP!O~#r#s0g~0lOy~~0oP#p#q%]~0wO!P~~0zP#q#r0}~1SOz~",
21
+ tokenizers: [0, 1, 2],
22
+ topRules: { Program: [0, 2] },
23
+ specialized: [{ term: 17, get: (value) => spec_Keyword[value] || -1 }],
24
+ tokenPrec: 892,
25
+ });
26
+
27
+ module.exports = { parser };
minimal-validator.js ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ // Minimal Liftoscript validator using Lezer parsers
4
+ // No dependencies on the full Liftosaur codebase
5
+
6
+ const { parser: liftoscriptParser } = require('./liftoscript-parser.js');
7
+ const { parser: plannerParser } = require('./planner-parser.js');
8
+
9
+ function getLineAndColumn(text, position) {
10
+ const lines = text.split('\n');
11
+ let currentPos = 0;
12
+
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const lineLength = lines[i].length + 1; // +1 for newline
15
+ if (position < currentPos + lineLength) {
16
+ return {
17
+ line: i + 1,
18
+ column: position - currentPos + 1
19
+ };
20
+ }
21
+ currentPos += lineLength;
22
+ }
23
+
24
+ return { line: lines.length, column: 1 };
25
+ }
26
+
27
+ function detectScriptType(script) {
28
+ // Detect if this is planner syntax or pure Liftoscript
29
+ const plannerIndicators = [
30
+ /^#\s+Week/m,
31
+ /^##\s+Day/m,
32
+ /^\s*\w+\s*\/\s*\d+x\d+/m, // Exercise format like "Squat / 3x5"
33
+ /\/\s*progress:/,
34
+ /\/\s*warmup:/,
35
+ /\/\s*update:/
36
+ ];
37
+
38
+ return plannerIndicators.some(regex => regex.test(script)) ? 'planner' : 'liftoscript';
39
+ }
40
+
41
+ function validateLiftoscript(script) {
42
+ try {
43
+ const scriptType = detectScriptType(script);
44
+ let tree;
45
+
46
+ if (scriptType === 'planner') {
47
+ // Parse with planner parser
48
+ tree = plannerParser.parse(script);
49
+ } else {
50
+ // Parse as pure Liftoscript
51
+ tree = liftoscriptParser.parse(script);
52
+ }
53
+
54
+ // Check for error nodes
55
+ let hasError = false;
56
+ let errorNode = null;
57
+ let errorType = null;
58
+
59
+ tree.iterate({
60
+ enter: (node) => {
61
+ if (node.type.isError) {
62
+ hasError = true;
63
+ errorNode = node;
64
+ errorType = node.type.name;
65
+ return false; // Stop iteration
66
+ }
67
+ }
68
+ });
69
+
70
+ if (hasError && errorNode) {
71
+ const { line, column } = getLineAndColumn(script, errorNode.from);
72
+
73
+ // Try to provide more helpful error messages
74
+ let message = `Syntax error`;
75
+ const problemText = script.substring(errorNode.from, Math.min(errorNode.to, errorNode.from + 20));
76
+
77
+ if (scriptType === 'planner') {
78
+ if (problemText.includes('/')) {
79
+ message = "Invalid exercise format. Expected: 'Exercise / Sets x Reps'";
80
+ } else if (problemText.includes(':')) {
81
+ message = "Invalid property format. Expected: 'property: value' or 'property: function(args)'";
82
+ }
83
+ } else {
84
+ if (problemText.includes('=')) {
85
+ message = "Invalid assignment. Check variable names and syntax";
86
+ } else if (problemText.includes('{') || problemText.includes('}')) {
87
+ message = "Unmatched braces";
88
+ }
89
+ }
90
+
91
+ return {
92
+ valid: false,
93
+ error: {
94
+ message: `${message} at "${problemText.trim()}"`,
95
+ line: line,
96
+ column: column,
97
+ type: scriptType
98
+ }
99
+ };
100
+ }
101
+
102
+ // No syntax errors found
103
+ return {
104
+ valid: true,
105
+ error: null,
106
+ type: scriptType
107
+ };
108
+ } catch (e) {
109
+ return {
110
+ valid: false,
111
+ error: {
112
+ message: `Parser error: ${e.message}`,
113
+ line: 0,
114
+ column: 0
115
+ }
116
+ };
117
+ }
118
+ }
119
+
120
+ // Export for use
121
+ module.exports = { validateLiftoscript };
122
+
123
+ // CLI interface
124
+ if (require.main === module) {
125
+ const fs = require('fs');
126
+ const args = process.argv.slice(2);
127
+
128
+ if (args.length === 0) {
129
+ console.log('Liftoscript Validator');
130
+ console.log('Usage: node minimal-validator.js <script or -> [--json]');
131
+ console.log('\nExamples:');
132
+ console.log(' node minimal-validator.js "state.weight = 100lb"');
133
+ console.log(' node minimal-validator.js - < program.liftoscript');
134
+ console.log(' echo "Squat / 3x5" | node minimal-validator.js - --json');
135
+ process.exit(0);
136
+ }
137
+
138
+ let script;
139
+ const jsonOutput = args.includes('--json');
140
+
141
+ if (args[0] === '-') {
142
+ // Read from stdin
143
+ script = fs.readFileSync(0, 'utf-8');
144
+ } else {
145
+ script = args[0];
146
+ }
147
+
148
+ const result = validateLiftoscript(script);
149
+
150
+ if (jsonOutput) {
151
+ console.log(JSON.stringify(result, null, 2));
152
+ } else {
153
+ if (result.valid) {
154
+ console.log(`✅ Valid ${result.type} syntax`);
155
+ } else {
156
+ console.error(`❌ Invalid syntax (${result.error.type || 'unknown'} mode)`);
157
+ console.error(` Line ${result.error.line}, Column ${result.error.column}: ${result.error.message}`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+ }
package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "liftoscript-mcp-validator",
3
+ "version": "1.0.0",
4
+ "description": "Minimal Liftoscript validator for MCP server",
5
+ "main": "minimal-validator.js",
6
+ "scripts": {
7
+ "test": "node test-validator.js"
8
+ },
9
+ "dependencies": {
10
+ "@lezer/lr": "^1.3.14"
11
+ },
12
+ "devDependencies": {},
13
+ "keywords": ["liftoscript", "validator", "mcp"],
14
+ "author": "",
15
+ "license": "MIT"
16
+ }
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ nodejs
2
+ npm
3
+ git
planner-parser.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Planner parser - compiled from Lezer grammar
2
+ // This parses the Week/Day/Exercise format
3
+ const { LRParser } = require("@lezer/lr");
4
+
5
+ const spec_Keyword = { __proto__: null, none: 148 };
6
+ const parser = LRParser.deserialize({
7
+ version: 14,
8
+ states:
9
+ "2SQVQPOOOOQO'#D`'#D`OkQQO'#CcO|QQO'#CbOOQO'#D^'#D^OOQO'#Dk'#DkOOQO'#D_'#D_QVQPOOOOQO-E7^-E7^O!XQSO'#CeO!oQSO'#DbO#SQQO,58|OOQO,58|,58|O#SQQO,58|OOQO-E7]-E7]OOQO'#Cf'#CfO#[QSO,59PO#_QSO,59PO#gQWO'#CfOOQO'#Cl'#ClO#rQSO'#CwO#}QPO'#C{O$SQSO'#CkOOQO'#DR'#DRO$XQWO'#DUO$aQSO'#DVO%RQSO'#DWO#}QPO'#DXOOQO'#Df'#DfO%sQSO'#DSO&TQSO'#DQO!^QSO'#DQO&cQQO'#DYO&qQQO'#CjOOQO,59|,59|OOQO-E7`-E7`OOQO1G.h1G.hO&|QQO1G.hO!XQSO,59SO!XQSO'#DaO'UQSO1G.kOOQO1G.k1G.kOOQO,59o,59oOOQO'#Cs'#CsO'^QSO,59cOOQO,59c,59cOOQO,59g,59gO(RQSO,59VO!XQSO,59pO(dQWO,59pOOQO,59q,59qOOQO,59r,59rO(iQPO,59sOOQO-E7d-E7dO!^QSO'#DgO(qQSO,59lO(qQSO,59lO)PQWO'#DZOOQO,59t,59tO)XQPO,59UOOQO7+$S7+$SOOQO1G.n1G.nO)^QSO,59{OOQO,59{,59{OOQO-E7_-E7_OOQO7+$V7+$VOOQO1G.}1G.}OOQO'#Co'#CoO)iQQO'#CnO)}QWO'#DOOOQO'#Dd'#DdO*iQSO'#C}O+QQSO'#C|OOQO'#DP'#DPOOQO1G.q1G.qO,QQSO1G/[O+`QSO1G/[O!XQSO1G/[OOQO1G/_1G/_OOQO,5:R,5:ROOQO-E7e-E7eO,XQSO1G/WOOQO'#D['#D[O,gQSO,59uOOQO1G.p1G.pO,oQSO'#CzOOQO,59Y,59YO-`QSO,59YO!XQSO,59jOOQO-E7b-E7bO-jQSO'#DeO-uQSO,59hOOQO7+$v7+$vO.uQSO7+$vO.TQSO7+$vOOQO1G/a1G/aO)PQWO1G/aO.|QPO,59fO/RQSO'#CfO/^QSO'#ChOOQO'#Cq'#CqO/cQSO'#CqO/kQSO'#CxOOQO'#Cp'#CpO,tQSO'#DcO/sQSO1G.tO/{QQO1G.tO/sQSO1G.tOOQO1G/U1G/UOOQO,5:P,5:POOQO-E7c-E7cOOQO<<Hb<<HbO0^QPO7+${O0cQPO'#CcOOQO1G/Q1G/QOOQO,59],59]O0kQSO,59dO1PQSO,59dOOQO,59},59}OOQO-E7a-E7aO1UQQO7+$`OOQO7+$`7+$`O1gQSO7+$`OOQO<<Hg<<HgOOQO1G/O1G/OO1oQSO1G/OOOQO<<Gz<<GzO2TQQO<<GzOOQO7+$j7+$jOOQOAN=fAN=fO#}QPO'#C{",
10
+ stateData:
11
+ "2u~O!^OS~OQTORTOSTOTTOWPO!pSO~OWPO]VX!`VX!qVX!oVX~O]YO!`XO!q[O~OZ_O~OZbOiiOjjO!ekO!fdO~OacO!ieO!mgO]!UX!q!UX~P!^O]YO!qtO~O!avO!bwO!cyO~O!nzOfYX!kYX~OZ{Of}Oh{O~OWPO~O!d!PO~Of!RO!k!QO~Of!SOZyX]yXiyXjyX!byX!eyX!fyX!oyX!qyX~Of!TOZzX]zXizXjzX!bzX!ezX!fzX!ozX!qzX~O]vX!bvX!ovX!qvX~P!^O!b!WO]tX!otX!qtX~O!`!ZO]|X!o|X!q|X~O!o!]O]^X!q^X~O]YO!q!^O~O!bwO!c!cO~Of!dOZka]kaikajka!bka!eka!fka!oka!qka!gka~OZ_Oa!eOi!hOj!hO!l!kO~O!k!oO~OWPO!g!pO~O!b!WO]ta!ota!qta~OZ!tO!P!tO~O!p!vO~O!avO!b!Ta!c!Ta~Om!xO!e!yO!h!wO]bX!obX!qbX~O!k!zOZrX]rXirXjrX!brX!orX!qrX~OZ_Oi!hOj!hO]qX!bqX!oqX!qqX~O!b!|O]pX!opX!qpX~Of#OOZxi]xiixijxi!bxi!exi!fxi!oxi!qxi~O!avO~P+`O!b!WO]ti!oti!qti~O!c#RO!d#SO~O!i#vO~OZ#UOa#YOf#XOh{Oi#ZOj#ZO!a#XO!fdO~O!b#[O!g#^O~P,tOZ_Oi!hOj!hO~O!b!|O]pa!opa!qpa~Of#cOZxq]xqixqjxq!bxq!exq!fxq!oxq!qxq~O!avO~P.TO!j#fO~O!aYX!bgX!ggX~O!avO~OZ{Oh{O~Of#iO!d#hO~O!b#[O!g#lO~Om#mO!h!wO]bi!obi!qbi~O!c#oO~OWPO!jVX~OZ{Of#XOh{Oi#pOj#pO!a#XO~O!d#qO~Om#rO!h!wO]bq!obq!qbq~O!b#[O!g#sO~OZ{Of#XOh{Oi#tOj#tO!a#XO~Om#uO!h!wO]by!oby!qby~Oiajh!o!`!cWTZSRQR~",
12
+ goto:
13
+ "'|!`PPPPPP!a!eP!m!pP#cP#s#v#yP#|$P$S$YP$dPPP$p#oP$z%W#|%^%d#|#v%i%l%u%u%u%u%u#v%{&OP!a&U&[&l&s&}'X'`'f'nPPP'xTTOVSROVT!Oe#vR]RQ`XWhYmo!WQ!_vQ!`wU!g!P!i!|Q!m!QQ#P!oS#V!y#[R#`!zQaXQ!awQ!n!QQ#Q!oT#Z!y#[RrYRqYRfYR!l!PR!f!PQ#_!yR#j#[S#Z!y#[Q#p#hR#t#qQ|dW#W!y#[#h#qR#g#XWlYmo!WT#Z!y#[Q!x!fQ#m#^Q#r#lR#u#sQpYR#T!wQ!j!PR#a!|V!h!P!i!|RoYQnYQ!YoR!q!WXlYmo!WR![pQ!u!ZR#d#SQVOR^VUQOVeUWQ!U#eQ!UkR#e#vSx`aR!bxQZRSsZuRu]Q#]!yS#k#]#nR#n#_S!i!P!|R!{!iQ!}!jR#b!}UmYo!WR!VmQ!XnS!r!X!sR!s!YTUOV",
14
+ nodeNames:
15
+ "⚠ Program LineComment TripleLineComment Week Day ExerciseExpression ExerciseName NonSeparator Repeat Rep Int RepRange SectionSeparator ExerciseSection ExerciseProperty ExercisePropertyName Keyword FunctionExpression FunctionName FunctionArgument Number Plus PosNumber Float Weight Percentage Rpe KeyValue Liftoscript ReuseLiftoscript ReuseSection WarmupExerciseSets WarmupExerciseSet WarmupSetPart None ExerciseSets CurrentVariation ExerciseSet Timer SetPart WeightWithPlus PercentageWithPlus SetLabel ReuseSectionWithWeekDay WeekDay WeekOrDay Current EmptyExpression",
16
+ maxTerm: 79,
17
+ skippedNodes: [0],
18
+ repeatNodeCount: 9,
19
+ tokenData:
20
+ "KQ~R{OX#xXY$|YZ%XZ]#x]^%`^p#xpq$|qr%jrs#xst&jtx#xxy*lyz*qz{#x{|*v|}3z}!O4z!O!P6Q!P!Q:i!Q![>s![!]AZ!]!b#x!b!cBZ!c!}CZ!}#ODj#O#PDo#P#QEo#Q#R#x#R#SEt#S#T#x#T#gCZ#g#hGV#h#lCZ#l#mHh#m#oCZ#o#pIy#p#q#x#q#rJv#r;'S#x;'S;=`$v<%l~#x~O#x~~J{R#}]WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#xR$yP;=`<%l#x~%RQ!^~XY$|pq$|_%`O!pP!q^_%gP!pP!q^YZ%XV%q]!mSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x~&mZOY'`YZ(OZ]'`]^(T^s'`st({t;'S'`;'S;=`(u<%l~'`~O'`~~(O~'cXOY'`YZ(OZ]'`]^(T^;'S'`;'S;=`(u<%l~'`~O'`~~(O~(TOS~~(YXS~OY'`YZ(OZ]'`]^(T^;'S'`;'S;=`(u<%l~'`~O'`~~(O~(xP;=`<%l'`~)OXOY({YZ)kZ]({]^)r^;'S({;'S;=`*f<%l~({~O({~~)k~)rOT~S~~)yXT~S~OY({YZ)kZ]({]^)r^;'S({;'S;=`*f<%l~({~O({~~)k~*iP;=`<%l({~*qO!e~~*vO!g~_*}_f[WROX#xZ]#x^p#xqs#xtx#xz!O#x!O!P+|!Q![2g![!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_,R^WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q![,}![!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_-ScWROX#xZ]#x^p#xqs#xtu#xuv._vx#xz!P#x!Q![,}![!}#x#O#P#x#Q#_#x#_#`/_#`#a1c#a#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_.f]j[WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_/d_WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#Z#x#Z#[0c#[#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_0j]i[WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_1h_WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#U#x#U#V0c#V#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_2ldWROX#xZ]#x^p#xqs#xtu#xuv._vx#xz!O#x!O!P+|!Q![2g![!}#x#O#P#x#Q#_#x#_#`/_#`#a1c#a#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_4R]!b[WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_5R_!aSWROX#xZ]#x^p#xqs#xtx#xz!O#x!O!P+|!Q![2g![!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_6V_WROX#xZ]#x^p#xqs#xtx#xz!O#x!O!P7U!Q![9V![!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#xV7Z^WROX#xZ]#x^p#xqs#xtx#xz!O#x!O!P8V!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#xV8^]!iSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_9^chSWROX#xZ]#x^p#xqs#xtu#xuv._vx#xz!P#x!Q![9V![!}#x#O#P#x#Q#_#x#_#`/_#`#a1c#a#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_:nP]^!P!Q:qP:tZOY;gYZ<VZ];g]^<[^!P;g!P!Q=S!Q;'S;g;'S;=`<|<%l~;g~O;g~~<VP;jXOY;gYZ<VZ];g]^<[^;'S;g;'S;=`<|<%l~;g~O;g~~<VP<[OQPP<aXQPOY;gYZ<VZ];g]^<[^;'S;g;'S;=`<|<%l~;g~O;g~~<VP=PP;=`<%l;gP=VXOY=SYZ=rZ]=S]^=y^;'S=S;'S;=`>m<%l~=S~O=S~~=rP=yORPQPP>QXRPQPOY=SYZ=rZ]=S]^=y^;'S=S;'S;=`>m<%l~=S~O=S~~=rP>pP;=`<%l=S_>zdWRZ[OX#xZ]#x^p#xqs#xtu#xuv._vx#xz!O#x!O!P@Y!Q![>s![!}#x#O#P#x#Q#_#x#_#`/_#`#a1c#a#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x_@_^WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q![9V![!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#xVAb]!dSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#xVBb]!fSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#xVCbbaSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q![CZ![!c#x!c!}CZ#O#P#x#Q#R#x#R#SCZ#S#T#x#T#oCZ#p#q#x#r;'S#x;'S;=`$v<%lO#x~DoO!`~~Dv]!o~WROX#xZ]#x^p#xqs#xtx#xz!P#x!Q!}#x#O#P#x#Q#o#x#p#q#x#r;'S#x;'S;=`$v<%lO#x~EtO!c~_E}b!PWaSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q![CZ![!c#x!c!}CZ#O#P#x#Q#R#x#R#SCZ#S#T#x#T#oCZ#p#q#x#r;'S#x;'S;=`$v<%lO#x_G`b!nWaSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q![CZ![!c#x!c!}CZ#O#P#x#Q#R#x#R#SCZ#S#T#x#T#oCZ#p#q#x#r;'S#x;'S;=`$v<%lO#x_Hqb!kWaSWROX#xZ]#x^p#xqs#xtx#xz!P#x!Q![CZ![!c#x!c!}CZ#O#P#x#Q#R#x#R#SCZ#S#T#x#T#oCZ#p#q#x#r;'S#x;'S;=`$v<%lO#x~JOP!h~#r#sJR~JUTO#rJR#r#sJe#s;'SJR;'S;=`Jp<%lOJR~JhP#q#rJk~JpOm~~JsP;=`<%lJR~J{O!j~^KQO!q^",
21
+ tokenizers: [0, 1, 2, 3],
22
+ topRules: { Program: [0, 1] },
23
+ specialized: [{ term: 17, get: (value) => spec_Keyword[value] || -1 }],
24
+ tokenPrec: 804,
25
+ });
26
+
27
+ module.exports = { parser };
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio[mcp]>=5.0.0
2
+ requests>=2.31.0