MX Script
A modern, open-source scripting language for building web apps and APIs.
One file. Zero boilerplate. Run with mx run app.mx.
Created and developed by Jassim Alkharafi.
server { port: 8080 }
let db = sql.open("./users.db")
sql.migrate(db, [
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"
])
group /api/v1 {
get /users {
return json(sql.query(db, "SELECT * FROM users"))
}
post /users {
let r = validate(request.body, {
type: "object",
required: ["name", "email"],
properties: {
name: { type: "string", min_length: 2 },
email: { type: "string", format: "email" }
}
})
if (!r.valid) { return status(400, { errors: r.errors }) }
let res = sql.exec(db, "INSERT INTO users (name, email) VALUES (?, ?)",
request.body.name, request.body.email)
return status(201, { id: res.last_insert_id })
}
}
get /openapi.json { return json(openapi({ title: "Users API" })) }
get /docs { return swagger_ui("/openapi.json") }
That's a real validated REST API with persistence and auto-generated docs. mx run app.mx and you're done.
Why MX Script?
Most languages make you choose: ergonomics or speed. JavaScript is fast to write but you assemble a framework, a runtime, a build tool, and a deploy story before you ship anything. Go is fast and simple but the syntax for a JSON API is a hundred lines.
MX Script is opinionated: the language is the framework. Routes, JSON, SQL, JWT, OAuth, WebSockets, AI, and background jobs are all first-class. The interpreter is a single Go binary.
What ships in the box
| Layer |
What you get |
| Web framework |
Routes (get /users { ... }), middleware, route groups, static files, cookies, CORS, gzip, rate limiting, body limits, timeouts, graceful shutdown, TLS |
| Real-time |
Server-sent events (sse /events) and WebSockets (ws /chat) — both pure stdlib, no external dependencies |
| Database |
SQLite + Postgres through one sql namespace. Transactions, hash-tracked migrations, parameterized queries |
| Auth |
JWT, signed-cookie sessions, OAuth2 helpers (Google / GitHub / Discord / LinkedIn / Microsoft), password.hash (PBKDF2-SHA256), AES-256-GCM, webhook signature verification |
| AI |
OpenAI / Anthropic Claude / Google Gemini through ai.complete. Streaming, tool calling, embeddings |
| Background jobs |
Durable, SQLite-backed queue with retries and exponential backoff |
| API tooling |
openapi() auto-generates a 3.1 spec from your routes. swagger_ui() and redoc_ui() mount interactive docs in one line |
| Stdlib (~200 fns) |
Strings, arrays (map / filter / reduce / sort / group_by / zip), math, JSON, URL parsing, regex, file I/O, image manipulation, markdown rendering, CSV, email (SMTP), templates, schedulers, validation, subprocess, fs.watch |
| Concurrency |
spawn { ... } goroutines, channels, wait_group, thread-safe environments |
| Tooling |
mx run / init / new / build / repl / test [--cover] / bench / fmt / lsp / upgrade / doctor |
| Editor support |
TextMate grammar, VS Code extension, full LSP (diagnostics, format-on-save, hover, completion for all builtins) |
| Distribution |
GoReleaser binaries on every tag, Homebrew tap, mx build --vercel adapter |
Six starter projects, one command
mx new api my-api # REST API + OpenAPI + Swagger UI + status page
mx new todo my-todos # JWT auth + SQLite + groups + validate
mx new chat realtime # WebSockets + sessions + broadcast
mx new ai my-bot # Tool-calling agent (5-turn loop)
mx new blog my-blog # SSR markdown blog with admin
mx new saas my-saas # Magic-link auth + Stripe + /metrics + cron + admin
Each scaffolds a complete, runnable app. Read the source, change the bits you don't like, ship.
Try it in your browser
The full interpreter compiles to WebAssembly. Run a program without installing anything:
mx build --wasm # produces dist/mx.wasm + wasm_exec.js
# then serve site/playground/ — open the result in any modern browser
The playground at site/playground/ ships a complete editor + run loop with bundled examples (closures, map/filter, match, JSON, tight numeric loops). See site/playground/README.md for deploy instructions.
Install
Option 1 — pre-built binary (coming soon)
Download from the releases page and put mx on your $PATH.
Option 2 — build from source
Requires Go 1.21+.
git clone https://github.com/jlkdevelop/mxscript.git
cd mxscript
go build -o mx .
./mx version
Option 3 — go install
go install github.com/jlkdevelop/mxscript@latest
The binary is named mxscript after go install. Rename or alias it to mx:
mv $(go env GOPATH)/bin/mxscript $(go env GOPATH)/bin/mx
Quickstart
mx init my-api
cd my-api
mx run app.mx
Then in another terminal:
curl http://localhost:8080/
curl http://localhost:8080/hello/Jassim
CLI
mx run <file.mx> # run an MX Script program
mx run <file.mx> --port 3000 # override the server port
mx run <file.mx> --watch # restart on file changes (hot reload)
mx run <file.mx> --debug # print tokens and AST
mx test [path] # run *_test.mx files
mx init [name] # scaffold a new project
mx build <file.mx> # parse & validate without running
mx repl # interactive REPL
mx version # print version
mx help # show help
Language tour
Variables
let name = "Jassim"
let age = 30
let active = true
let scores = [10, 20, 30]
let user = { id: 1, name: "Ada" }
// String interpolation
print("Hello, ${name}! Score sum: ${scores[0] + scores[1] + scores[2]}")
// Optional chaining + nullish coalescing
let city = user?.profile?.city ?? "unknown"
Functions
fn greet(name) {
return "Hello, " + name
}
print(greet("world"))
Anonymous functions:
let doubled = map([1, 2, 3], fn(n) { return n * 2 })
Control flow
if (age >= 18) {
print("adult")
} else {
print("minor")
}
loop scores as s {
print(s)
}
loop 5 as i {
print(i)
}
let n = 0
while (n < 100) {
n = n + 1
if (n == 50) { break }
}
HTTP routes
// Shorthand form (recommended)
get / { return text("hello") }
get /users/:id { return json({ id: request.params.id }) }
post /users { return status(201, request.body) }
delete /users/:id { return json({ deleted: true }) }
// Verbose form (equivalent)
route GET /users { return json(users) }
Middleware
middleware auth {
if (request.headers["authorization"] != "Bearer secret") {
return status(401, { error: "unauthorized" })
}
}
route POST /admin {
use auth
return json({ ok: true })
}
Built-in functions
| Category |
Functions |
| Output |
print, println |
| HTTP |
json, text, html, status, redirect, fetch |
| Env |
env(name) |
| String |
len, upper, lower, split, trim, contains, replace, starts_with, ends_with |
| Array |
push, pop, map, filter, find, join, reverse, range |
| Math |
round, floor, ceil, abs, min, max, random |
| Types |
typeof, isString, isNumber, isBool, isNull, isArray, isObject |
| JSON |
json_parse, json_stringify(v, pretty?) |
| File I/O |
read_file, write_file, file_exists, list_files, delete_file |
| Crypto |
hash_sha256, hmac_sha256, base64_encode, base64_decode, uuid |
| Regex |
re_match, re_find, re_find_all, re_replace |
| JWT |
jwt.sign(payload, secret), jwt.verify(token, secret) |
| URL |
parse_url, url_encode, url_decode |
| Time |
now(), now_iso(), sleep(ms), parse_date, format_date |
| Test |
assert(cond, msg?), assert_eq(a, b, msg?) |
| KV store |
kv_get, kv_set, kv_delete, kv_keys, kv_clear |
| Misc |
retry(fn, attempts, delay_ms?) |
| AI |
ai.complete(prompt), ai.embed(text) |
See docs/api.md for the full reference.
AI built-ins
route POST /summarise {
let summary = ai.complete("Summarise: " + request.body.text)
return json({ summary: summary })
}
ai.complete(prompt, opts?) works against ten providers behind a single API. Pass { provider: "..." } to switch:
| Provider |
provider: value |
API key env var |
| OpenAI (default) |
"openai" |
OPENAI_API_KEY |
| Anthropic Claude |
"anthropic" |
ANTHROPIC_API_KEY |
| Google Gemini |
"gemini" / "google" |
GEMINI_API_KEY |
| xAI Grok |
"grok" / "xai" |
XAI_API_KEY |
| Mistral |
"mistral" |
MISTRAL_API_KEY |
| DeepSeek |
"deepseek" |
DEEPSEEK_API_KEY |
| Groq |
"groq" |
GROQ_API_KEY |
| OpenRouter |
"openrouter" |
OPENROUTER_API_KEY |
| Together AI |
"together" |
TOGETHER_API_KEY |
| Ollama (local) |
"ollama" |
none — runs locally |
ai.stream(prompt, on_chunk, opts?), ai.embed(text), ai.vision(prompt, images) and ai.similarity(a, b) round out the namespace. See examples/ai_providers.mx for a copy-pasteable cheat sheet.
Pattern matching
let label = match status_code {
200 => "OK"
404 => "Not Found"
500 => "Server Error"
_ => "Status ${status_code}"
}
Authentication with JWT
route POST /login {
let token = jwt.sign({ sub: request.body.username, exp: now() / 1000 + 3600 }, env("JWT_SECRET"))
return json({ token: token })
}
route GET /me {
let claims = jwt.verify(request.headers["authorization"], env("JWT_SECRET"))
if (claims == null) {
return status(401, { error: "invalid token" })
}
return json(claims)
}
Error handling
try {
let data = json_parse(request.body.payload)
return json(data)
} catch (e) {
return status(400, { error: e.message })
}
Deploy to Vercel
MX Script ships a built-in Vercel adapter. From any project with an app.mx:
mx build --vercel
git add main.go go.mod vercel.json
git commit -m "Deploy via mx build --vercel"
git push # Vercel autodeploys on push
mx build --vercel generates three files at the project root:
| File |
Role |
main.go |
A 30-line Go entrypoint that //go:embeds app.mx, lexes/parses/loads it via the interpreter library, and serves the resulting handler on $PORT. |
go.mod |
Pins the mxscript runtime to the version of the CLI that generated the build. |
vercel.json |
Declares Vercel's Go framework preset so the build is detected automatically. |
Vercel does the rest: detects the Go module, compiles the binary, and runs it as a long-running serverless function on Fluid Compute. Your .mx source is the source of truth — re-run mx build --vercel anytime you upgrade the mx CLI.
Embedding MX in your own Go server? The same building blocks that power the Vercel adapter — interpreter.Load(prog), interpreter.Handler(), interpreter.HasRoutes() — are public. Drop an MX app into any Go binary that speaks net/http.
Editor support
.mx files have full syntax highlighting in VS Code (and any TextMate-compatible editor):
git clone https://github.com/jlkdevelop/mxscript.git
cd mxscript/extras/vscode
code --install-extension .
The grammar lives at extras/syntax/mxscript.tmLanguage.json — drop it into Sublime, Atom, or any other TextMate-aware editor.
GitHub itself doesn't yet recognise .mx natively (we're working through github-linguist), but the .gitattributes in this repo gives the best fallback.
Project structure
mxscript/
├── main.go # CLI entry point
├── lexer/lexer.go # tokenizer
├── parser/
│ ├── ast.go # AST node types
│ └── parser.go # recursive-descent parser
├── interpreter/
│ ├── interpreter.go # tree-walking interpreter + HTTP server
│ ├── builtins.go # standard library
│ └── parse_helper.go # bridge for `import "./file.mx"`
├── examples/ # sample .mx programs
└── docs/ # full language reference
Roadmap
See ROADMAP.md. Highlights:
- Lexer, parser, interpreter
- HTTP routes (GET / POST / PUT / DELETE / PATCH)
-
request.body, request.params, request.headers, request.query
- Middleware via
use
- Control flow (
if, else, loop ... as)
-
try / catch
- Standard library (string, array, math, JSON, types)
-
ai.complete / ai.embed
- CLI:
--port, --watch, --debug, mx init, mx build
- Local imports:
import "./utils.mx"
- Pre-built binary releases for macOS / Linux / Windows
- Package registry (
mx install pkg)
- WebSocket routes
- Built-in SQLite driver
- LSP for editor support
Contributing
MX Script is open source under the MIT license. Pull requests, issues, and discussions are welcome.
See CONTRIBUTING.md for the full workflow. The TL;DR:
git clone https://github.com/jlkdevelop/mxscript.git
cd mxscript
go build -o mx .
go test ./...
./mx run examples/app.mx
License
MIT © Jassim Alkharafi
Credits
MX Script was created by Jassim Alkharafi (creator & lead developer). If MX Script is useful to you, a ⭐ on GitHub goes a long way.