- Published on
SubJS: JS Runtime for Agents. Let your agent do what it does best.
- Authors

- Name
- Emilio González
- @emi0x7d1
The problem with MCP servers
I've been trying to use MCP servers regularly since the protocol was introduced back in 2024. It was amusing to me to see these LLMs improve so fast at writing code that I could leave them unsupervised, yet I never felt like I could rely on MCP servers. The agents would struggle to call an MCP tool to do a simple REST request in the same conversation they would output an impressive 500 LOC TypeScript file that would run on the first try.
The problem becomes obvious when you look at the agent output and it looks something like this:
> Tool call: playwright `browser_navigate`
> Thinking...
> Tool call: playwright `browser_click`
> Thinking...
> Tool call: playwright `browser_click`
> Thinking...
> Tool call: playwright `browser_type`
> ...
When the agent requires multiple tool calls to complete a task, inference must happen between every call. This makes it hard to compose tools without polluting the context apart from being incredibly inefficient.
SubJS
SubJS is an MCP server and CLI program that exposes the Deno runtime to agents. It lets agents create sessions where JavaScript code can be evaluated and the program state is persisted across evaluations.
With Deno, a Node.js-compatible JavaScript runtime doing the powerlifting, your agents can take advantage of the massive amount of JS/TS in their training data to execute tasks.
In the previous case, instead of going back and forth between the Playwright MCP server and the agent, the agent can simply output the following command or execute the equivalent MCP tools:
$ subjs session new my-session
$ subjs run -s my-session '
import { chromium } from "npm:playwright"
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
await page.goto("...")
await page.click(...)
await page.click(...)
await page.type("...")
'
and Deno will fetch the dependencies on-demand. In this way, we let the Agent do what it does best: write code! (especially TypeScript code).
We can continue evaluating code in the same session:
$ subjs run -s my-session '
console.log(await page.locator("body").textContent());
'
# output: ...PAGE BODY...
and then finally terminate it
subjs session terminate my-session
It might sound strange to talk about how MCPs are bad and provide SubJS as an MCP server but it's the only standardized protocol to expose tools and it does the job well if we are just exposing one main tool.
Pairing with Agent Skills
When paired with Agent Skills, SubJS becomes even more useful. We transform it from a generic "do-anything" tool, no different than a shell tool, into metaprograms that make your agent way more capable.
A skill to manipulate images looks as simple as this (where sharp is a JS library for manipulating images):
---
name: image-editing
description: Edits and transforms images
---
When a user asks for an image to be edited, resized or converted, use subjs
with the `sharp` library.
If you are feeling fancy, you might add some examples but the advantage of letting our agent execute JavaScript is that it's so good at it that it's unnecessary for many tasks.
For good and for bad, the NPM ecosystem is huge and has almost everything your agent might need.
Security
The main advantage of using Deno over other runtimes is its permission system. Because of that, you might be thinking that was the reason Deno was chosen as the runtime but we actually don't use permissions at all. The decision comes from the assumption that we can trust agents.
Because of this, the recommendation is to only use SubJS if you are already experienced working with AI-assisted tools.
For now, the only way we can really secure agents is with proper sandboxing which Deno (the runtime, not the company) does not provide.
Motivation
I am not the first one to have this question. In fact, Armin Ronacher explains the problem better than I could in "Tools: Code Is All You Need", then he later explores the same approach in this Your MCP Doesn’t Need 30 Tools: It Needs Code.
Before SubJS, I had forked Deno to create a prototype that I kept hacking on but had no time to polish. When I decided to start over from scratch and started reading Deno's source code again, I realized I could use the --json flag in Deno's repl to connect it to an MCP server. I'm still missing debugger support which is also very important and coming soon.
But my real motivation for releasing SubJS is to hopefully bring a bit of attention to a personal issue as ashamed as I am to be writing this.
I would appreciate it if you took the time to read:
A few weeks ago my girlfriend started having serious health issues and we are struggling to pay the medical bills. I am making less than 1000 USD per month as a contractor for an American company and we have to pay ~1500 USD in a few days so you might imagine how desperate we are feeling right now as I am our only source of income
I am a 24 year old self-taught programmer from Mexico who has been programming since the age of 10 and I am looking for a job as a software engineer. I went to college two years but had to dropout to work and haven't been able to get a stable foothold since.
Please, see my resume here if you are interested
I am willing to work as a contractor or normal job.
The nature of the project might make some people think I quickly vibecoded this project specifically for this section so here is some evidence of me not being a vibecoder:
- 17-year-old me developing a game in a minimal vim install, no lsp, no autocomplete, no syntax highlighting (2019)
- Spanish programming language I wrote when I was 17
- Wrote about advanced Rust
- Released well-received Rust tool
I know they're not impressive but their purpose is to show a bit of credibility in an internet written by LLMs.
Get it
SubJS is open-source and it's hosted on GitHub.
I hope you find it useful.