cd /news/ai-tools/show-hn-static-mcp-publish-mcp-tools… · home topics ai-tools article
[ARTICLE · art-74854] src=github.com ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Show HN: Static MCP – publish MCP tools as static files, run them sandboxed

Static MCP, a new open-source project, lets developers publish MCP tools as static, hash-verified files executed sandboxed on demand, eliminating the need for server infrastructure. The reference implementation, mcpwasm, runs untrusted tool code inside QuickJS-wasm on Cloudflare Workers and integrates with the llms-txt-skills standard. Users can run tools from any static site via a single npx command, with no account or deploy required.

read39 min views1 publishedJul 27, 2026
Show HN: Static MCP – publish MCP tools as static files, run them sandboxed
Image: source

Static MCP: your tools are files, not servers. Tools are published as static, hash-verified content and executed sandboxed on demand. What static site hosting did to web servers — "don't run Apache, publish HTML" — this does to MCP servers: don't run an MCP server, publish files. The publisher needs zero infrastructure; the MCP server is materialized per request from those files and evaporates after responding (ephemeral instance, durable definition).

mcpwasm is the reference implementation: a sandboxed runtime for third-party MCP tools (untrusted tool code inside QuickJS-wasm on Cloudflare Workers), plus a gateway that turns any static site publishing llms.txt

with executable skills into an MCP server.

Think "php-wasm, but for MCP tools": the platform owner embeds the host, loads tool.js

files, and each tool runs isolated in a QuickJS WebAssembly sandbox. The only bridge from the sandbox to the platform's internals is an explicit capability the host injects. No capability, no access.

This repo integrates with the llms-txt-skills standard via two provisional extensions adopted in the spec: executable skills (v0.4, with origin memory) and skill attestations (v0.4). See the dedicated sections below.

Point the local runtime at any origin that publishes llms.txt

with executable skills. It fetches /llms.txt

, verifies every tool_sha256

, sandboxes each skill in QuickJS-wasm, and speaks MCP over stdio — no account, no deploy, no infrastructure on either side:

npx -y @rckflr/mcpwasm https://usuario.github.io

Wire it into an MCP client (Claude Code, Cursor, Cline, …):

{
  "mcpServers": {
    "misitio": {
      "command": "npx",
      "args": ["-y", "@rckflr/mcpwasm", "https://usuario.github.io"]
    }
  }
}

That is the whole path from a static site to a running MCP server. The two other ways to use mcpwasm are for when you specifically need them: the hosted gateway (multi-tenant, on Cloudflare Workers — serves many origins behind one endpoint, with an onboarding step per origin) and the embeddable library (@rckflr/mcpwasm

— for building your own host). Both are documented below; the local runtime above needs none of it. Its honest limits and the index.json

/attestation options are detailed under "Local MCP runtime".

The runtime is a standard MCP stdio server, so every MCP host can use it. Replace the origin with any publisher (find more in the registry); add --lock skills.lock

for pin-on-first-use.

Claude Code (one command):

claude mcp add static-skills -- npx -y @rckflr/mcpwasm https://mauricioperera.github.io

Claude Desktop (claude_desktop_config.json

) / Cursor (.cursor/mcp.json

) / project-scoped .mcp.json — same JSON shape:

{
  "mcpServers": {
    "static-skills": {
      "command": "npx",
      "args": ["-y", "@rckflr/mcpwasm", "https://mauricioperera.github.io", "--lock", "skills.lock"]
    }
  }
}

Restart the session and the origin's verified tools (plus their SKILL.md recipes as resources) appear like any other MCP server's.

The embeddable host — what the gateway itself builds on — ships as @rckflr/mcpwasm:

npm install @rckflr/mcpwasm
js
import { AsyncToolHost } from "@rckflr/mcpwasm";

const host = new AsyncToolHost({ allowedOrigin: "https://example.com" });
await host.init();
host.loadToolSource(toolJsSource); // a tool.js that calls registerTool({...})
const tools = host.listTools();
const result = await host.callTool("sum_numbers", { a: 2, b: 40 });
host.dispose();

Notes:

  • In Cloudflare Workers, pass a pre-built asyncify module via the quickjs

option (seeworker-gateway.mjs

for theCompiledWasm

import pattern and import@rckflr/mcpwasm/shim

first). - Subpath exports: /host

(syncToolHost

),/host-async

,/mcp-core

,/mcp-core-async

,/llmstxt-parse

,/shim

. - The sync ToolHost

lazy-imports the optional peerquickjs-emscripten

unless you pass a pre-built module; the async host's dependencies install with the package. - The package contains only the host/core/parser files plus the local runtime binary; the workers, publisher sites and test suites stay in this repo (they are the deployed reference, not the library).

The package also ships a stdio MCP server that runs an origin's skills locally: it fetches /llms.txt

, verifies every tool_sha256

, loads each verified skill into its own QuickJS-wasm context, and speaks MCP over stdin/stdout — so a static site (e.g. a GitHub Pages user site) becomes an MCP server on your machine with zero deployed infrastructure on either side:

npx -y @rckflr/mcpwasm https://usuario.github.io

MCP client configuration (Claude Code, Cursor, …):

{
  "mcpServers": {
    "misitio": {
      "command": "npx",
      "args": ["-y", "@rckflr/mcpwasm", "https://usuario.github.io"]
    }
  }
}

Publishers under a path work. The origin may carry one — a GitHub Pages project site (https://user.github.io/REPO

) is discovered at <base>/llms.txt

, not at the host root, and host.fetchOrigin

is scoped to that subpath rather than to the whole host, so one project cannot reach another's endpoints on a shared host. The same canonical origin is used by the gateway and by scripts/attest.mjs

, so an attestation signed for a project site verifies. (Pointing at a raw GitHub URL still does not work — see --serve

below.)

Honest limits (stated in bin/mcpwasm-local.mjs

): discovery runs once per process (restart to refresh). Hash verification and the sandbox model (per-skill contexts, origin-scoped fetchOrigin

, resource limits) are the same as the gateway's. Origin memory is supported: if the origin declares skills-memory

, the snapshot is fetched, verified against snapshot_sha256

, and host.memorySearch

is injected — same contract as the gateway; on any mismatch (or if the optional @rckflr/minimemory

engine is missing) the capability is simply absent and skills that call it fail closed in-sandbox. Each verified skill's SKILL.md

recipe is served as an MCP resource and via the get_skill_guide

tool (see "Skill recipes as MCP resources"). Trust is your choice of origin by default (no attestation required); Sigstore attestation verification is available opt-in via --require-attestation

(below). Also cross-checks tool_sha256

against .well-known/agent-skills/index.json

when the origin publishes one (see "Cross-checking against index.json"). Tested by npm run local

(hermetic, localhost-only; part of the CI gate — including verified-snapshot search and the tampered-snapshot fail-closed path).

Both the local runtime and the gateway now also fetch /.well-known/agent-skills/index.json

— the canonical metadata layer the core llms-txt-skills RFC defines (§8 Open Question 5: llms.txt

is the zero-fetch discovery pointer, index.json

is the metadata/verification source of truth). When a skill's name appears in both llms.txt

and index.json

, and the latter declares a tool_sha256

, it must match the one declared in llms.txt

; a mismatch rejects the skill (drift/tampering signal) exactly like a tool_sha256

mismatch against the fetched tool.js

itself. Absence of index.json

(most origins today) changes nothing.

Platform limitation, discovered during implementation:thesigstore

npm package depends on@sigstore/tuf

to cache Fulcio/Rekor's trusted root via TUF, and that cache usesnode:fs

with no way to bypass it through the public API. Cloudflare Workers has no filesystem, so Sigstore verificationonly runs in the local Node runtime(bin/mcpwasm-local.mjs

) — the gateway's attestation model remains the pre-registered-key Ed25519 scheme above, unaffected. This is the one capability asymmetry between the two runtimes; see[Capability support by runtime]. The spec (v0.4 §2.4) now names this profile explicitly: Sigstore is the RECOMMENDED default where verification can reach Sigstore's trust infrastructure; pre-registered Ed25519 is the profile for platforms — like Workers — where it cannot.

The Ed25519 model (above) requires pre-registering every reviewer's public key — a real bottleneck (today, only the maintainer is registered). Sigstore verifies any OIDC identity (a GitHub Actions workflow, a Google/GitHub login) without pre-coordination; the runtime's trust decision is which identity to require, not which key to whitelist — closer to what core RFC §4.6 recommends for identity-bound provenance.

npx -y @rckflr/mcpwasm https://usuario.github.io \
  --require-attestation "https://token.actions.githubusercontent.com|https://github.com/OWNER/REPO/.github/workflows/release.yml@refs/heads/main"

When set, discovery additionally fetches /.well-known/agent-skills/attestations.json

and, for every skill, requires a matching entry (origin

  • skill

  • tool_sha256

) whose sigstore_bundle

verifies against exactly that issuer|identity

pair, within its [signed_on, valid_until]

window. A skill without one — absent attestations.json, no matching entry, expired, or an invalid/mismatched bundle — is excluded, same treatment as a tool_sha256

mismatch. This flag is fail-closed by design: it is opt-in, but once set, absence of a valid attestation is not tolerated (unlike the gateway's advisory

mode).

The attestation object's signed payload is an in-toto Statement v1 inside a DSSE envelope, whose predicate

must carry the same 5 fields as the Ed25519 model (origin

, skill

, tool_sha256

, signed_on

, valid_until

) — verified to match the attestation's own top-level fields, so a validly-signed bundle for skill A cannot be relabeled as skill B's attestation without re-signing. sigstore-attest.mjs

exports verifySigstoreAttestation

(the verifier) and buildSigstoreStatement

(the canonical payload shape a publisher signs with sigstore attest

or an equivalent SDK call — this repo does not ship a signing tool for it, since producing a real Sigstore signature needs a live OIDC flow, out of scope for a script run here).

Verified against a real, live, publicly fetched Sigstore bundle (the SLSA provenance attestation for the sigstore@5.0.0

npm package's own publish, https://registry.npmjs.org/-/npm/v1/attestations/sigstore@5.0.0

) — proving the underlying Fulcio cert-chain + Rekor transparency-log verification genuinely runs and succeeds, and that a schema-mismatched or wrong-identity bundle is correctly rejected. All 6 --require-attestation

rejection paths (no attestations.json, empty array, no matching skill, malformed date, expired, invalid/empty bundle) verified end-to-end against bin/mcpwasm-local.mjs

. Honest gap: producing a positive fixture (a real Sigstore signature over this repo's own canonical payload, verifying as attested

) needs a live OIDC signing flow this environment cannot complete headlessly — untested is the happy path specifically, not the security-critical rejection paths.

Pointing the runtime at a raw GitHub URL does not work: new URL(...).origin

keeps only scheme+host+port, so https://raw.githubusercontent.com/you/repo/main/

collapses to https://raw.githubusercontent.com

— the you/repo/main

part (and therefore /llms.txt

) is gone. --serve

is the practical alternative: it starts an internal static file server (bound to 127.0.0.1

only, never exposed to the network) over a local directory — e.g. your own git clone

of a skills repo — and uses that as the origin, combining "serve this directory" and "connect to it" into one command:

npx -y @rckflr/mcpwasm --serve ./my-skills-repo

This is meant for developing and testing your own skills locally before publishing them (to GitHub Pages or any other static host) — not for browsing someone else's GitHub repo directly. Path-traversal requests against the internal file server are rejected (resolved and checked against the served directory's root); covered by npm run local

.

MCP clients (Claude, Cursor, others) can call arbitrary tools. Running a third-party tool's code directly in your backend means that code can read your secrets, hit your DB, phone home, or loop forever. You either trust the author fully or you don't run the tool.

mcpwasm removes the trust requirement for the code:

  • The tool runs in a separate QuickJS-wasm context with no host globals beyond what the host predefines ( registerTool

,host

). Nofetch

, noprocess

, no disk, no secrets. - The platform secret (e.g. a Stripe key) stays on the host side. The tool can only ask the host to perform a namedinternal action viahost.callInternal

(sync host) or a scopedhost.fetchOrigin

(async host). The host decides what is allowed. - The tool's tool.js

is content-addressed by SHA-256; the gateway refuses to load it if the hash declared inllms.txt

does not match the bytes it fetched. - Resource limits bound what a malicious/buggy tool can do: memory cap, stack cap, a deterministic gas budget (interrupt-handler invocation count), and a wall-clock fetch deadline per call.

In mcpwasm, publishing (static files + a hash) and execution (a runtime that discovers, verifies, and sandboxes on demand) are two separate things. In a traditional MCP server they are the same thing — the server you deploy is the execution, with no isolation layer in between.

Static MCP — local (npx @rckflr/mcpwasm ) | Static MCP — gateway | Traditional MCP server | | |---|---|---|---| | What the publisher ships | Static files: llms.txt + tool.js (+ SKILL.md ) | Same static files | A running server process (any language) | | Infrastructure the publisher operates | None — GitHub Pages, R2, any static host | None — same | The whole server: uptime, scaling, patching, secrets | | Where the tool code runs | Your own machine, in a QuickJS-wasm sandbox | The gateway Worker, in the same sandbox | The publisher's own process, natively, no isolation | | Integrity guarantee | SHA-256 verified before any byte executes | Same | None built in — you trust the deployed binary/image as-is | | Third trust ring (human review) | Not enforced (v1 limit — trust is your choice of origin) | Ed25519 attestations, enforcing mode in production | No standard mechanism | | Transport | stdio (JSON-RPC over stdin/stdout) | HTTP POST (JSON-RPC); needs the gateway URL + a token | Either — but fixed per implementation | | Network hops for the MCP call itself | Zero (local process); the tool can still call out via fetchOrigin | Two: client → gateway → publisher origin | Zero for local stdio, one for remote HTTP | | Measured overhead (this repo's own benchmarks) | Not separately benchmarked — same sandbox cost, no gateway hop | ~2 ms sandbox warm, ~6 ms for the full gateway vs. a direct API call ( |

The takeaway that doesn't fit in a table: a traditional MCP server answers "how do I expose this logic as a tool?" Static MCP additionally answers "how do I run code from an origin I don't fully trust?" If you write and control the server yourself, traditional is simpler and none of this is necessary. Static MCP matters when the tool code comes from someone else, and you want a verifiable guarantee (hash + sandbox, optionally review) before running it — that is the problem this repo exists to solve, not a general-purpose alternative to MCP.

You do not need to build or maintain an MCP protocol server — that is the whole point. The runtime (local or gateway) already handles JSON-RPC, tools/list

, tools/call

, and the transport; none of that is your code.

What you still have to write is not prose. A tool.js

per action is real, small glue code: it validates args

against the schema you declared, calls your existing API through host.fetchOrigin

, and shapes the response — see bookstore/content/create_order.tool.js

in this repo for a concrete example (validates qty

and book_id

, handles a 409 for insufficient stock as a distinct case, never lets a malformed call reach your backend). This is a different, stronger mechanism than a SKILL.md

with no tool.js

: prose-only skills are the core RFC's basic mode — an agent reads them and improvises the HTTP call with whatever generic request tool it has, with no schema validation, no sandbox, and no hash pinning. That is the "execution gap" that executable skills (this repo's reference feature) close. Handing an agent a raw "make any HTTP request" capability against your API reintroduces the problem this project exists to avoid — your backend ends up validating against an arbitrary caller either way; a tool.js

does that validation before your API is ever hit, and the agent only ever gets the specific, parameterized actions you defined.

"Zero infrastructure" is literal for internal use — your own team pointing npx @rckflr/mcpwasm

(or --serve

) at your published skills needs no server on either side. For external clients to reach you without installing anything, you need one endpoint answering MCP over HTTP; that means either your origin gets added to an existing deployed gateway's ALLOWED_ORIGINS

, or you wrangler deploy

your own instance of the same generic gateway code in this repo, configured for your origin. Either way it is a one-time, tool-agnostic deploy — not a bespoke MCP server built per API.

                         (1) publish llms.txt + tool.js + SKILL.md
   Publisher site  ───────────────────────────────────────────┐
   (static: R2/Pages/                                          │
    any host serving /llms.txt)                                │
                                                               ▼
                                                        ┌───────────────┐
   MCP client  ──POST /mcp?origin=<pub>──►  Gateway Worker │ discovers     │
   (Claude,                                            │ llms.txt,     │
    Cursor, ...)                                        │ verifies     │
        ▲                                               │ sha256 per    │
        │  (5) JSON-RPC response                         │ skill,        │
        └──────────────────────────────────────────────  │ loads tools   │
                                                        │ in sandbox    │
                                                        └──────┬────────┘
                                                               │ (2) per request:
                                                               │     new QuickJS
                                                               │     context,
                                                               │     origin-scoped
                                                               ▼
                                                        ┌───────────────┐
                                                        │ AsyncToolHost │  (3) tool code
                                                        │ (QuickJS-wasm │      calls
                                                        │  asyncify)    │      host.fetchOrigin
                                                        │               │────► (4) host fetches
                                                        │  mem/stack/   │      ONLY the allowed
                                                        │  interrupt    │      origin, returns
                                                        │  limits set   │      {status,body,
                                                        │               │       truncated,...}
                                                        └───────────────┘

Flow:

  • A publisher site ships /llms.txt

plus per-skilltool.js

andSKILL.md

files.llms.txt

lists each executable skill with a SHA-256 of itstool.js

, and may declare anorigin memorysnapshot (see below). - On each request the gateway downloads llms.txt

, parses the executable skills, downloads eachtool.js

, verifies SHA-256, and loads the verified ones into a freshAsyncToolHost

scoped to that origin. - Tool code runs inside QuickJS-wasm. It can only call host.fetchOrigin(path, opts?)

(opts

:{method: "GET"|"POST", body?: string ≤16 KB, contentType?}

— write skills go through POST; returns{status, body, truncated, bytes, contentLength}

, see the limits section for whattruncated

means and why it exists), which is async from the host side but synchronous-looking inside the sandbox (QuickJS asyncify suspends/resumes the wasm stack). An origin that declares a verified memory snapshot additionally getshost.memorySearch(query, k?)

. - The host fetches only the allowed origin; any other origin throws inside the sandbox.

  • The gateway maps MCP tools/list

andtools/call

over JSON-RPC 2.0 and returns the result to the client.

Pieces live in:

host.mjs

— synchronousToolHost

(sync tools,host.callInternal

capability).host-async.mjs

AsyncToolHost

(async handlers,host.fetchOrigin

  • theextraCapabilities

mechanism that backshost.memorySearch

, resource hardening).mcp-core.mjs

/mcp-core-async.mjs

— JSON-RPC 2.0 MCP core (transport-agnostic).worker.mjs

— PoC MCP server (sync host, inline tools).worker-spike.mjs

— async spike (fetchHome/fetchEvil).worker-gateway.mjs

+llmstxt-parse.mjs

— the gateway.worker-memspike.mjs

— memory spike: the docs-site origin published and served through the gateway end-to-end (host.memorySearch

over a BM25 snapshot), exercised bymf-memspike.mjs

.

Status: Draft v0.5, adopted.This format is specified by the[Executable Skills extension]of the[llms-txt-skills]standard. This repo is its reference implementation; the spec and this code are kept aligned (every MUST in the spec is field-tested here).

Under a ## Skills

section, an executable skill is a normal markdown list item followed by an HTML comment carrying a JSON object with version

, tool

(path to the tool.js

), and tool_sha256

(hex SHA-256 of the tool.js

bytes):

- [sum_numbers](/skills/sum_numbers/SKILL.md): Sum two numbers a and b. <!-- skill: {"version":"1.0.0","tool":"/skills/sum_numbers/tool.js","tool_sha256":"58daf86111bf7278446eb7e0e8c6384713b50cdb6fa97ac039e23846d723dc3e"} -->

Parsed by llmstxt-parse.mjs

:

  • The <!-- skill: {...} -->

comment marks the line as anexecutableskill. List items without it are treated as descriptive-only and ignored by the gateway. tool

is resolved relative to the origin.tool_sha256

is verified against the fetchedtool.js

bytes before the tool is loaded. Mismatch → the skill is rejected (logged) and not registered.- If the JSON is invalid the line is silently skipped (no throw).

A tool.js

registers itself:

registerTool({
  name: "sum_numbers",
  description: "Sum two numbers a and b.",
  inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
  handler(args) { return Number(args.a) + Number(args.b); }
});

Spec:

origin memoryin[Executable Skills v0.5].

Both runtimes.Origin memory runs in thegateway(worker-gateway.mjs

) and in thelocal runtime(bin/mcpwasm-local.mjs

), with the same contract: snapshot fetched, verified againstsnapshot_sha256

, and only then ishost.memorySearch

injected. In the local runtime the BM25 engine (@rckflr/minimemory

) is anoptionalDependency— installed by default withnpx

/npm install

; if it is missing, the runtime says so on stderr and degrades to "no memory" (capability absent, skills that call it fail closed in-sandbox — spec-conformant, not a crash).

A publisher that wants its skills to search over its own static content (docs, catalog text, any corpus) declares a memory snapshot with a single HTML comment before the ## Skills

section (this ordering is required by the reference parser):

<!-- skills-memory: {"snapshot":"/skills-index.snapshot","snapshot_sha256":"a0235f071aa7e28f2096312f22f1ad035901595f3fa91d2cc92b5879bbb7f6d5","format":"minimemory-okf-v1"} -->

## Skills
...

snapshot

is a path (relative to the origin) to a BM25 snapshot in theminimemory-okf-v1

format (built by theengine; the wasm binary ships as@rckflr/minimemory

minimemory_bg.wasm

).snapshot_sha256

is the hex SHA-256 of the snapshot bytes. Both runtimes download the snapshot andverify it against this hash before injecting the capability— same content-addressing rule astool.js

. On mismatch, fetch failure, non-200, or an unsupportedformat

, the snapshot is discarded andthe capability is not injected.- When the snapshot verifies, the runtime injects host.memorySearch(query, k?)

into every skill of that origin (via theextraCapabilities

bridge inAsyncToolHost

, same raw-JSON asyncify pattern ashost.fetchOrigin

).k

defaults to 5 and is clamped to[1, 10]

. It returns{ hits: [{ text, score, title, concept_id }] }

(or{ error }

). - Without a verified snapshot the capability is absent: a skill that calls host.memorySearch

seesundefined

and throwsinside the sandbox, surfacing asisError: true

(controlled failure, not a runtime crash). The skills still list — only the memory capability is missing.

The reference publisher is the docs-site (see "Repository layout"): it serves the spec snapshot and a search_spec

skill that runs host.memorySearch(args.q, k)

to do BM25 search over the four llms-txt-skills documents, plus get_doc

and list_docs

.

Discovery, tool_sha256

content-addressing, sandboxed tool.js

execution, origin memory (host.memorySearch

), skill recipes as MCP resources, and Ed25519 attestations work on both runtimes. One capability remains asymmetric, for a declared platform reason, not a bug:

| Capability | Gateway (worker-gateway.mjs , Workers) | Local runtime (bin/mcpwasm-local.mjs , Node) | |---|---|---| Origin memory — host.memorySearch | ✅ full | ✅ full (engine is an optionalDependency; if missing, degrades to capability-absent) | | Sigstore (keyless) attestations | ❌ Workers has no node:fs for @sigstore/tuf 's trust-root cache | ✅ --require-attestation |

Consequence: the local runtime is the full-featured reference — memory and Sigstore both work there. The gateway matches it except for Sigstore verification, where its attestation model remains Ed25519-only (platform limitation of Workers, documented in Sigstore above).

Hash pinning verifies bytes against what the publisher declares today. If the publisher — or whoever compromised their account — changes tool.js

and its declared hash together, a consumer receives the new code silently. --lock

closes that gap with pin-on-first-use:

npx -y @rckflr/mcpwasm https://example.com --lock skills.lock

First use pins each skill's declared

tool_sha256

and recipesha256

. - A later change is

rejected loudly(that skill only; the rest load):

skill rechazada: search_knowledge -> LOCK MISMATCH: el publicador cambio tool_sha256 …

If the change is a legitimate update, accept it explicitly:

npx -y @rckflr/mcpwasm https://example.com --lock skills.lock --lock-update

New skills are pinned with a notice. Memory snapshots are deliberately not locked — knowledge changes legitimately with every content update; code and instructions should not change without you noticing. Commit skills.lock

next to your agent config, like a package lock.

The third runtime, since 0.7.0.

Live demo:[https://mauricioperera.github.io/mcpwasm/demo/]— same trust model as the local runtime, with Node removed from the equation.

The whole consumer side runs in a browser tab: discovery, byte-for-byte SHA-256 verification (crypto.subtle

), one QuickJS-wasm sandbox per verified tool, scopes, per-scope origin memory and verified SKILL.md recipes. The publisher only needs CORS (GitHub Pages already serves Access-Control-Allow-Origin: *

).

import { connectStaticSkills } from "@rckflr/mcpwasm/web";

const skills = await connectStaticSkills("https://mauricioperera.github.io", {
  quickjsWasm: "./emscripten-module.wasm",   // URL, bytes or WebAssembly.Module
  // optional BM25 memory:
  minimemoryWasm: "./minimemory_bg.wasm",
  minimemoryInit: (bytes) => { initSync({ module: bytes }); return WasmOkfIndex; },
  onLog: console.log,
});
skills.tools;                                  // verified, scope-renamed
await skills.callTool("search_knowledge", { q: "how do I publish?" });
skills.recipes;                                // verified SKILL.md per tool

Bundle it with any bundler (esbuild --bundle --platform=browser

) and serve the two wasm files next to it — npm run build:web

does exactly that into docs/demo/

. The module is environment-agnostic: the same code runs in Node 20+, which is how CI smoke-tests it (npm run test:web

, hermetic local publisher).

This enables fully serverless agent stacks in the browser — e.g. a wasm-agents-style HTML agent whose tools are verified static skills: no server, no Node, nothing to install on either side.

Spec: §2.5 of

[Executable Skills v0.5](resolves RFC v0.10 Open Question 6). Both runtimes, since 0.6.0.

One origin (e.g. a GitHub Pages root site) can aggregate skills from several projects. Without namespacing, two projects that both publish a search_knowledge

tool collide, and only one skills-memory

line can exist per origin. Scopes fix both:

<!-- skills-memory: {"snapshot":"/KDD/skills-index.snapshot","snapshot_sha256":"…","format":"minimemory-okf-v1","scope":"kdd"} -->

## Skills

- [search_knowledge](/KDD/skills/search_knowledge/SKILL.md): … <!-- skill: {"version":"1.0.0","tool":"/KDD/skills/search_knowledge/tool.js","tool_sha256":"…","scope":"kdd"} -->

scope

(optional, pattern^[a-z][a-z0-9_-]*$

) declares the project namespace of a skill line. The runtime exposes the tool under the public name(<scope>__<toolName>

kdd__search_knowledge

); the rename happens at thehost boundary only— the publishedtool.js

bytes, itstool_sha256

, and any attestations are untouched. This preserves the universal-template property: the sametool.js

can be served by every publisher under different scopes with one ecosystem-wide hash.- One skills-memory

lineper scope(at most one withoutscope

). Each skill getshost.memorySearch

bound toits own scope's verified snapshot — memories are isolated per project. - An invalid scope

value makes the line non-executable (reported, not loaded). A public-name collision (two lines mapping to the same public name) keeps the first and skips the rest with a diagnostic. - Skill recipes follow the public name: skill://kdd__search_knowledge

. - Fully backward compatible: no scope

⇒ exactly the pre-0.6.0 behavior.

Both runtimes. An executable skill has two halves: the

recipe(SKILL.md

— when/how to use it, sequencing, constraints; the "recipe layer" the core RFC §3.3 defines) and thecapability(tool.js

). Serving only the tools loses the recipe — the agent gets the hammer without the manual.

Discovery also fetches each verified skill's SKILL.md

and verifies it against the sha256

declared in the same llms.txt

line (core RFC field). Verified recipes are exposed two ways:

MCP resourcesresources/list

/resources/read

, uriskill://<name>

,text/markdown

; the capability is advertised ininitialize

. The semantically correct path for clients that support it.— a synthetic, runtime-provided (not sandboxed) tool returning the recipe by skill name. Universal fallback: every MCP client supports tools.get_skill_guide

tool

Failure semantics mirror everything else here, but the halves are independent: a missing/unfetchable/hash-mismatched SKILL.md

omits the recipe (warned on stderr / console) while the tool — verified by its own tool_sha256

— loads unaffected. Under enforcing

attestation mode, an excluded skill's recipe is excluded with it. Size cap: MAX_SKILLMD_BYTES

(default 256 KB). Covered by npm run local

(verified recipe served, tampered recipe excluded, missing recipe tolerated).

Spec:

[Skill Attestations v0.4].

This section describes the gateway's model: Ed25519 signatures from a runtime-side pre-registered REVIEWERS

key registry. The local runtime additionally supports Sigstore (keyless) attestations via --require-attestation

— no pre-registered key, any OIDC identity the runtime explicitly trusts — see "Sigstore attestations" above; that section closes the "only one registered reviewer scales" bottleneck this one has.

A publisher may serve a third trust ring — signed reviewer attestations — at /.well-known/agent-skills/attestations.json

. Each entry is an Ed25519 signature over the canonical payload and has this shape:

{
  "origin": "https://llmstxt-docs.rckflr.workers.dev",
  "skill": "search_spec",
  "tool_sha256": "95301993...",
  "attester": "human:mauricio",
  "signed_on": "2026-07-02",
  "valid_until": "2027-07-02",
  "signature": "<base64 Ed25519 signature>"
}

The signed payload is the UTF-8 bytes of origin + "\n" + skill + "\n" + tool_sha256 + "\n" + signed_on + "\n" + valid_until

with origin

canonical (lowercase, no trailing slash, no default port) and tool_sha256

lowercase hex. The gateway:

  • Fetches attestations.json

during discovery (only when attestations are notoff

). A 404 or malformed array means "no attestations" (every skill isunattested

), not a discovery error. - Verifies each signature with WebCrypto ( Ed25519

viacrypto.subtle

, public key imported raw) against the runtime-side reviewer registryREVIEWERS

(aREVIEWERS

var inwrangler-gateway.toml

mappingattester → { public_key: <base64 raw 32 bytes>, registered_at }

). An attester not in the registry is ignored; a registered attester whose signature fails marks the skillinvalid

. - Computes a per-skill verdict with precedence invalid > attested > expired > unattested(invalid

dominates): a matching attestation from a registered reviewer with a valid signature inside its[signed_on, valid_until]

window isattested

; valid signature outside the window isexpired

; no matching attestation isunattested

. - Exposes the verdicts two ways: a tag appended to each tool's description

intools/list

, and a summary headerX-Gw-Attestations

(attested=N,expired=N,invalid=N,unattested=N

) on every response.

Three modes via ATTESTATION_MODE

:

off

(default whenATTESTATION_MODE

is unset) — attestations are not fetched; behavior is the pre-T25 gateway.advisory

— everything loads; verdicts are visible but do not exclude.enforcing

(deployed since T45) — onlyattested

skills load; non-attested

skills are excluded exactly like atool_sha256

mismatch (logged, not registered).

scripts/attest.mjs

is the signing tool (Node node:crypto

Ed25519, no deps):

node scripts/attest.mjs keygen

— generates an Ed25519 pair, writes the private key to.attester-key.json

and printsonly the public key(base64 raw 32 bytes) for theREVIEWERS

registry.node scripts/attest.mjs sign <origin> <skill> <valid_until>

— fetches the origin's livellms.txt

, reads the realtool_sha256

for the skill, signs, and prints the attestation object JSON.… sign <origin> --all <valid_until>

— signsevery executable skill thellms.txt

declares and prints the whole array, ready to paste intoattestations.json

. Signing one at a time is how the third one gets forgotten.… --llms <file>

/… --from-worker <file>

— read thellms.txt

from alocal source instead of the live origin: a file, or theLLMS_TXT

embedded in a freshly built worker. This is what lets you signbeforedeploying. Reading the live origin forces the opposite order — deploy, then sign — and underenforcing

that leaves a window where the new hashes have no matching attestation, so the gateway excludes those skills entirely.

The private key lives in .attester-key.json

and is local and gitignored — never commit it, and it is never printed by the tool. No key material belongs in this repo or in REVIEWERS

(only public keys).

Third-party publishers (sites you do not control): see ONBOARDING.md for the eligibility, review, attestation, activation, and revocation process.

Requirements: Node 18+ and npm install

(already done in this checkout).

npm install
npm test      # build + e2e Miniflare for the sync PoC (worker.mjs)
npm run spike # build + e2e Miniflare for the async spike (worker-spike.mjs)
npm run gateway # build + e2e Miniflare for the gateway (worker-gateway.mjs) — hits the live demo site
npm run memspike # build the memory snapshot + memspike worker, then e2e Miniflare against the docs-site origin (host.memorySearch / BM25)

The rest of the suites, all hermetic (no network) and all part of the CI gate:

npm run gateway:offline # the gateway suite with fake origins — no network at all
npm run local     # the stdio runtime end to end against a fake publisher
npm run subpath   # publishers living under a path (project sites), both runtimes
npm run test:web  # the browser runtime, running the same module in Node
npm run test:bundle  # the BUILT demo bundle, i.e. the artifact GitHub Pages serves
npm run test:attest  # sign offline with attest.mjs, verify the real gateway accepts it

And the drift guards, which fail if a committed generated artifact no longer matches its source — the failure mode that let the demo bundle sit five fixes behind its own source across four merges:

npm run check:bundle      # docs/demo/mcpwasm-web.js
npm run check:docs-site   # docs-site/worker.mjs (+ snapshot, provenance)
npm run check:publishers  # demo-site/ and bookstore/ workers

npm run gateway

is documented as-is from package.json

; it builds the gateway worker and runs mf-gateway.mjs

against the real deployed demo site. npm run memspike

does the same for the memory capability: build-memsnapshot.mjs

build-memspike.mjs

mf-memspike.mjs

.

The gateway is live at https://llmstxt-gateway.rckflr.workers.dev

. It is restricted to origins in its allowlist; the demo site https://llmstxt-demo-site.rckflr.workers.dev

, the bookstore https://llmstxt-bookstore.rckflr.workers.dev

(D1-backed, includes a write skill create_order

), and the docs-site https://llmstxt-docs.rckflr.workers.dev

(origin memory / BM25) are allowed. origin

is URL-encoded as a query param. The deployed gateway has auth enabled: every request below needs -H "Authorization: Bearer <AUTH_TOKEN>"

(the AUTH_TOKEN

secret; 401 otherwise). The token is a secret — it is not in this repo. The deployed gateway can also run in per-client mode (the CLIENTS

secret), in which case each client sends its own Authorization: Bearer <client_token>

with the same curl syntax; the response then carries X-Gw-Client: <client_id>

.

List the skills the demo site publishes:

curl -s -X POST \
  "https://llmstxt-gateway.rckflr.workers.dev/mcp?origin=https%3A%2F%2Fllmstxt-demo-site.rckflr.workers.dev" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Call sum_numbers

(pure sync tool, runs in the sandbox):

curl -s -X POST \
  "https://llmstxt-gateway.rckflr.workers.dev/mcp?origin=https%3A%2F%2Fllmstxt-demo-site.rckflr.workers.dev" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"sum_numbers","arguments":{"a":2,"b":40}}}'

Call server_time

(async tool that calls host.fetchOrigin("/api/time")

on the allowed origin):

curl -s -X POST \
  "https://llmstxt-gateway.rckflr.workers.dev/mcp?origin=https%3A%2F%2Fllmstxt-demo-site.rckflr.workers.dev" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"server_time","arguments":{}}}'

The other deployed workers (their root path returns 404 by design — only specific routes like /llms.txt

are served):

  • PoC sync host: https://toolhost-mcp.rckflr.workers.dev

(POST/mcp

). - Demo publisher: https://llmstxt-demo-site.rckflr.workers.dev/llms.txt

. - Bookstore publisher: https://llmstxt-bookstore.rckflr.workers.dev/llms.txt

. - Docs publisher (origin memory): https://llmstxt-docs.rckflr.workers.dev/llms.txt

.

What it guarantees:

Tool-host isolation. Tool code runs in a QuickJS-wasm context separate from the Worker's JS. It sees onlyregisterTool

,host

, and what the host prelude defines. Nofetch

, noprocess

, no globals leak by default.Secrets stay outside the sandbox. In the sync PoC, the platform secret is read fromenv.STRIPE_SECRET

on the host side and is never exposed to tool code — the tool can only call named internal methods. In the gateway, there is no platform secret; the only capabilities arehost.fetchOrigin

and (when declared and verified)host.memorySearch

.SHA-256 content addressing. The gateway downloadstool.js

and verifies it against thetool_sha256

declared inllms.txt

before . Mismatched or corrupt content is rejected and not cached. The same rule applies to the origin-memory snapshot (snapshot_sha256

): unverified → capability not injected.Skill attestations (third trust ring, spec See the dedicated section. Publishers may serve signed reviewer attestations; the gateway verifies them via WebCrypto against the runtime-sideext-skill-attestations

v0.4).REVIEWERS

registry and exposes per-skill verdicts (attested

/expired

/invalid

/unattested

,invalid

dominates) in each tool description and theX-Gw-Attestations

header. Modes:off

(default when unset) /advisory

(everything loads, verdicts visible) /enforcing

(onlyattested

skills load; deployed mode since T45).scripts/attest.mjs

is the signing tool (keygen + sign).Origin-scoped fetch.host.fetchOrigin

only fetches the single allowed origin for the request. Any other origin throwsinside the sandboxand is surfaced asisError: true

, not a JSON-RPC error.Resource limits (defaults inhost-async.mjs

, applied per request):- memory: 64 MB ( setMemoryLimit

) - stack: 1 MB ( setMaxStackSize

) - deterministic gas: 20 000 interrupt-handler invocations per callTool

/loadToolSource

(setInterruptHandler

with an invocation counter). This is the primary cutoff, becauseDate.now()

freezes in Cloudflare Workers during synchronous execution (Spectre mitigation), so a purewhile(true){}

never advances the clock. The gas counter does not depend on the clock — it counts how many times QuickJS invoked the handler (calibrated ~100× over the heaviest legitimate skill; see TAREA12B). - execution budget: 2000 ms per callTool

/loadToolSource

(a cheap backstop where the clock does advance — Node/tests). It measuresexecution, not wall clock: time the stack spends suspended waiting for a host capability (fetchOrigin

,memorySearch

) does not count against it, so a slow origin no longer kills a tool that then tries to process the response. Network waits are bounded separately, per fetch, by the outbound deadline below. When either cutoff fires the error says which one ("gas agotado" vs "presupuesto de EJECUCION agotado") — they used to be the same bare "interrupted". - response body cap: 4096 bytes perhost.fetchOrigin

(maxResponseBytes

). The cap is enforced on bytes, so it bounds host memory regardless of encoding. Truncation isobservable: the tool receives{status, body, truncated, bytes, contentLength}

truncated

says whether the body was cut,bytes

how many were read, andcontentLength

the size the origin declared (ornull

when it declares none), so a tool can report the real size of a resource instead of the size of what it got. - outbound fetch deadline: 10 s per host.fetchOrigin

(AbortSignal.timeout

  • aPromise.race

backstop that fires even if the fetch impl ignores the signal; on firing it throws "fetchOrigin timeout" inside the sandbox →isError: true

, not a gateway crash).

  • memory: 64 MB ( Fresh context per request. A new QuickJS context (and runtime) is built per request and disposed at the end; no state survives between requests.Per-skill contexts in the gateway. Each skill is loaded into its own QuickJS context; a skill cannot see or overwrite another skill's registration or globals, even within the same origin.Concurrency safety. The gateway keeps a pool of up to N independent instances of the asyncify wasm module per isolate (WASM_POOL_SIZE

, default 4, clamped to [1, 8]; the compiledWebAssembly.Module

is shared, each instance has its own memory). Each request acquires one instance exclusively, so up to N requests run truly in parallel per isolate and the (N+1)-th waits by polling in its own request context (workerd cancels continuations of promises resolved from another request context, so a FIFO handoff is not viable) — with N=1 this degenerates to the previous per-module mutex (TAREA19). Discovery is single-flighted per origin so concurrent cold requests share one discovery pass.

What it does not guarantee:

Auth has three modes, selected by config. Precedence is per-client → legacy shared token → dev open.Per-client (CLIENTS

secret, opt-in).CLIENTS

is a JSON secret mappingsha256_hex_of_token → { client_id, rpm? }

. Tokens never appear in cleartext in config — the key is the lowercase hex SHA-256 of the token's UTF-8 bytes. OnPOST /mcp

the gateway hashes theAuthorization: Bearer <token>

value and does an exact lookup on that hash; the lookupisthe timing-safe mechanism (a fixed digest is compared, never the cleartext token against a secret). A known token passes and the response carriesX-Gw-Client: <client_id>

; an unknown token, missing header, or malformed header yields401

.AUTH_TOKEN

is ignored in this mode. IfCLIENTS

is set but its JSON is invalid, the gatewayfail-closes— everyPOST /mcp

returns401

rather than opening by config error (signalled onGET /

).*Legacy shared token (*IfAUTH_TOKEN

secret).CLIENTS

is unset, theAUTH_TOKEN

secret enables a single shared bearer token (constant-time comparison); the deployed gateway has it enabled. Without it the gateway runs open (dev mode). The PoC worker remains open.*Per-client rate limiting (opt-in, requires per-client mode).*When per-client mode is active, the client'srpm

is a non-null number, and theRATE_LIMITER

Durable Object binding is present, eachPOST /mcp

is counted against afixed window of 60 s persisted in the DO's SQLite-backed storage (one DO instance perclient_id

, keyed by name). Within quota, responses carryX-Gw-RateLimit-Limit

/-Remaining

/-Reset

;Remaining

countsincluding the current request(the admitted sequence showsrpm, rpm-1, … 1

). Over quota the gateway returns429

withRetry-After

andRemaining: 0

. Honest edges: a fixed window allows a burst of up to2 × rpm

straddling a window boundary (a client can spend a full window's quota at its tail and the next window's at its head), and the limiter is per-request, not per-cost — it caps call count, not payload size, CPU, or complexity. If the DO itself fails while the limiter is active, the gatewayfail-closes with an observable500 rate_limiter_unavailable

rather than letting the request through uncounted. Without the binding (or with norpm

), the limiter stays inactive and the request path is byte-identical to the prior behavior.

Per-skill isolation is context-level, not process-level. Skills get separate QuickJS contexts but share the same wasm module instance and the same Worker request; the boundary is the QuickJS API surface, not an OS process.One asyncify suspension at a time — per module instance. QuickJS asyncify suspends/resumes a single stack per wasm instance; within one request all execution is sequential on its instance. Cross-request parallelism comes from the instance pool (up toWASM_POOL_SIZE

concurrent requests per isolate), not from overlapping suspensions on one instance.State is in-memory and per-request. No persistence, no warm state between requests. A tool that accumulates state loses it when the request ends.DoS is bounded, not impossible. The limits above cap a single call's cost; a determined caller can still spend the limits' worth of CPU/memory per request. Discovery is cached in two layers: layer 1 caches the parsed result per isolate for 60 s, and layer 2 caches the full post-verification result in the Cache API per colo for 60 s (observable via theX-Gw-Discovery: hit|l2|miss

response header —hit

served from layer 1,l2

hydrated cross-isolate from layer 2,miss

fetched from the origin). The layer 2 key carries a config fingerprint (attestation mode + reviewer registry + UTC date), so changing the config never serves stale verdicts. What is cached is post-verification (thetool.js

bytes were already hash-checked when layer 2 was populated), inside the account's own trust domain; the cold path is amortized more, but still not zero. A scheduled preheat (cron every minute,[triggers]

inwrangler-gateway.toml

) runs discovery for every allowlisted origin and instantiates a wasm module, so the cron's isolate/colo rarely serves a cold miss — honest caveat: the Cache API is per-colo and the cron runs in one location, so other colos still pay their first miss.The publisher is trusted for the skill list. The gateway trusts the origin's/llms.txt

to name skills; it verifies thetool.js

bytes match the declared SHA-256, but it does not vet what the tool does.

File / dir Purpose
CHANGELOG.md
Per-release changes of the published npm package (verified against the actual tarballs).
host.mjs
Synchronous ToolHost : loads tool.js into QuickJS-wasm, injects the host.callInternal capability.
host-async.mjs
AsyncToolHost : asyncify variant, async handlers, host.fetchOrigin capability, the extraCapabilities bridge (host.memorySearch ), mem/stack/gas hardening.
mcp-core.mjs
Sync MCP JSON-RPC 2.0 core (initialize , tools/list , tools/call , ping ). Transport-agnostic.
mcp-core-async.mjs
Async MCP core; awaits AsyncToolHost.callTool .
worker.mjs
PoC MCP server (sync host, inline tools) deployed at toolhost-mcp.rckflr.workers.dev .
worker-spike.mjs
Async spike (fetch_home/fetch_evil) proving origin-scoped fetch.
worker-gateway.mjs
The gateway: discover → verify → load → serve MCP, + origin-memory injection and attestations. Deployed at llmstxt-gateway.rckflr.workers.dev .
llmstxt-parse.mjs
Pure parser for the executable-skill lines (and the skills-memory line) of llms.txt . Also reports prose-only (nonExecutable ) skills found in ## Skills .
sigstore-attest.mjs
verifySigstoreAttestation / buildSigstoreStatement — Sigstore (keyless) attestation verification. Node-only (see "Sigstore attestations" above); used by bin/mcpwasm-local.mjs 's --require-attestation , not the gateway.
worker-memspike.mjs
Memory spike: docs-site origin served through the gateway with host.memorySearch over a BM25 snapshot.
internal-logic.mjs
Demo platform logic for the sync PoC (holds the secret, exposes createPayment /refundPayment ).
tools-inline.mjs
Inline tool.js sources for the sync PoC.
shim.mjs
location /self shim needed by the quickjs-emscripten wasm in Workers.
build.mjs / build-spike.mjs / build-gateway.mjs
esbuild bundlers (conditions workerd , external *.wasm ) for the PoC, spike, and gateway workers.
build-memspike.mjs / build-memsnapshot.mjs
esbuild bundler for the memspike worker, and the snapshot builder for the docs-site BM25 snapshot.
mf-test.mjs / mf-spike.mjs / mf-gateway.mjs / mf-memspike.mjs
e2e tests with Miniflare v4 against the built workers (PoC, spike, gateway, memspike).
wrangler.toml
Wrangler config for the PoC (sync) worker.
wrangler-gateway.toml
Wrangler config for the gateway. Vars: ALLOWED_ORIGINS (origin allowlist), REVIEWERS (attestation reviewer registry, JSON), ATTESTATION_MODE (off /advisory /enforcing ). Service bindings DEMO , BOOKSTORE , DOCS (same-account worker-to-worker fetch, bypassing Cloudflare error 1042). AUTH_TOKEN and CLIENTS are set as secrets, not in this file. Durable Object binding RATE_LIMITER (class RateLimiter , migration v1 with new_sqlite_classes ) deploys with the worker; the limiter stays inactive until a CLIENTS registry with rpm values exists.
scripts/attest.mjs
Attestation tool: keygen (writes local .attester-key.json , prints public key) and sign <origin> <skill> <valid_until> (Ed25519 attestation JSON).
bench/ + BENCHMARK.md
bench/run.mjs (single-client latency harness against the deployed workers) and its raw results; BENCHMARK.md is the write-up.
quickjs.wasm / quickjs-asyncify.wasm
Pre-compiled QuickJS binaries imported as static CompiledWasm modules.
minimemory_bg.wasm
Pre-compiled minimemory (BM25) wasm, the engine behind host.memorySearch . Imported as a static CompiledWasm module by the gateway.
demo-site/
Demo publisher site (llms.txt + sum_numbers / server_time skills). Deployed at llmstxt-demo-site.rckflr.workers.dev .
bookstore/
Realistic publisher: D1-backed catalog (52 books), read skills + create_order write skill, plus permanent robustness fixtures (corrupt_skill hash-mismatch, busy_loop infinite loop). Deployed at llmstxt-bookstore.rckflr.workers.dev .
docs-site/
Docs publisher: serves the llms-txt-skills spec documents + a skills-index.snapshot (BM25, minimemory-okf-v1 ), with search_spec (BM25 via host.memorySearch ), get_doc , and list_docs skills. Deployed at llmstxt-docs.rckflr.workers.dev .
reports/
Development reports, one TAREA*-REPORT.md per milestone (see below), plus the raw MCP-client outputs of T13-T15.
.github/workflows/ci.yml
GitHub Actions CI: two jobs (hermetic gate + prod-integration non-blocking) on push and pull_request to main .

The workflow in .github/workflows/ci.yml

runs two jobs on every push and pull_request to main

, both on ubuntu-latest

with Node 22 and npm ci

(with cache), timing out after 15 minutes.

The hermetic

job is the gate. It runs five local suites — npm test

, npm run spike

, npm run memspike

, npm run gateway:offline

, npm run local

— each preceded by its own build. None of these touch the network beyond npm

itself: test

, spike

, memspike

, and local

are fully local (the last spawns the stdio runtime against an in-process fake publisher on 127.0.0.1

), and gateway:offline

is the hermetic mode of the gateway suite (T35), where the production workers are replaced by in-process fakes served through the same URL-to-binding map the gateway uses. Hermeticity is enforced by an outbound fetch interceptor: if anything in the suite tries to leave the process for the network, the run fails. This job blocks the merge.

The prod-integration

job runs npm run gateway

, the online gateway suite against the deployed production workers (*.rckflr.workers.dev

) over the public internet. This is the only command in CI that reaches production, and its purpose is to detect drift between the fakes and the real workers. It is non-blocking (continue-on-error

): an outage on their side surfaces as a warning, not a red gate, so a foreign incident cannot block work in this repo.

Each milestone is documented in its reports/TAREA*-REPORT.md

(TAREA1 through TAREA45; TAREA2

and TAREA30

were skipped in numbering and TAREA12B

is a continuation of TAREA12). The non-obvious bits live there:

reports/TAREA4-REPORT.md

— deploying to Cloudflare Workers: theCompiledWasm

rule and why importing the.wasm

as a static module avoids "Wasm code generation disallowed by embedder".reports/TAREA5-REPORT.md

— the asyncify spike: why asyncify is needed for anawait

-shaped capability, and the promise-pumping loop inAsyncToolHost.callTool

.reports/TAREA7-REPORT.md

— the gateway: sha256 verification, the Cache API use, and the Cloudflare error 1042 (same-account worker-to-worker fetch viaworkers.dev

) workaround via a service binding.reports/TAREA12-REPORT.md

/reports/TAREA12B-REPORT.md

Date.now()

is frozen in Cloudflare Workers during synchronous execution, so a wall-clock deadline never cuts awhile(true){}

. Fix: a deterministic gas budget — the interrupt handler counts its own invocations and interrupts at 20 000, independent of the clock. Calibrated against the heaviest legitimate skill.reports/TAREA14-REPORT.md

structuredContent

in an MCP result must be a JSON object (MCP-shaped), not a bare scalar/array; the gateway normalizes tool output accordingly.reports/TAREA19-REPORT.md

— concurrency: a per-wasm-module mutex on instantiation plus single-flight discovery per origin, so parallel cold requests share one discovery pass and one module build.reports/TAREA22-REPORT.md

— origin memory: theskills-memory

line, sha256-verified BM25 snapshot, and thehost.memorySearch

capability injected viaextraCapabilities

.reports/TAREA25-REPORT.md

— skill attestations (Ed25519, WebCrypto,REVIEWERS

registry, verdicts, advisory/enforcing modes,scripts/attest.mjs

).reports/TAREA26-REPORT.md

— code-review fixes:extraCapabilities

now forwards all positional args (sohost.memorySearch(q, k)

keepsk

), and thefetchOrigin

timeout backstop timer is cleared on resolve (no leaked timers).

Benchmark headline numbers (full matrix and methodology in BENCHMARK.md, single-client from México to the Workers edge, not a load test; latest figures from the post-pool+preheat run): the sandbox itself costs

~2 ms warm(gateway pure-sandbox

sum_numbers

p50 ≈ 55 ms vs. the same worker's raw ping p50 ≈ 53 ms), and the full gateway adds ~6 ms over calling the publisher's API directly for the same read (

stock_report

through the gateway p50 = 96 ms vs. direct API p50 = 90 ms). A cold discovery miss costs ~210–400 ms (compile + sha256 + fetch); the scheduled preheat (see "Security model" above) keeps the cron's own isolate/colo mostly out of this cold path.Run the e2e tests with npm test

(sync) / npm run spike

(async) / npm run gateway

(gateway against the live demo site) / npm run memspike

(memory capability against the docs-site origin).

── more in #ai-tools 4 stories · sorted by recency
── more on @static mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-static-mcp-p…] indexed:0 read:39min 2026-07-27 ·