Skip to main content
Glama
cosmonic-labs

mcp-off-shelf

TypeScript MCP server template for WASI p3

The unmodified official @modelcontextprotocol/server 2.0.0 SDK runs in a self-contained WebAssembly component. dist/mcp.wasm exports wasi:http/handler@0.3.0 and imports wasi:http/client@0.3.0. HTTP is p3 in both directions. The component uses JCO's Node compatibility adapters and a patched QuickJS compiler. The MCP protocol core stays upstream. A small Rust p3 HTTP adapter handles native request cancellation; there is no Hono dependency.

The server supports MCP 2026-07-28 and legacy clients. Its default tools are add, echo, current_time, http_get, and http_request. It serves discovery, health, and embedded skills, and includes policy guards, deadlines, structured logs, optional native OTEL, and Cosmonic Desktop manifests.

Use this template

Create a repository from this GitHub template, clone it, and follow the build steps below. This is the TypeScript counterpart to the Rust template.

Customize the mcp-off-shelf application name consistently in package.json, package-lock.json, the skill frontmatter and URIs, workload manifests, .wash/config.yaml, and tests. Update the image registry, ingress hosts, and MCP domain labels for your server. Implement tools in src/server.ts and update the embedded playbook under skills/server/. Keep outbound destinations explicit in allowedHosts.

Related MCP server: MCP Server TypeScript

Parity with the Rust template

Compared against cosmonic-labs/mcp-server-template-rs at bbb0cc315c504a226eb23857d4bd6dcc3d0a5042:

Rust template feature

TypeScript implementation

Stateless official SDK, current MCP revision

Official SDK 2.0.0; modern and legacy official-client tests

Async p3 HTTP inbound and outbound

Both interfaces use 0.3.0; build rejects p2 HTTP imports

add, echo, current_time, http_get

All included; http_request adds methods, headers, redirect policies, and structured HTTP output

Discovery and health

GET/HEAD / and /health, outside the Host guard and body read

Skills extension

skills/list, skills/get, resources/directory/read, resource files, templates, instructions, and skill://index.json

Immutable skill manifest

Embedded YAML frontmatter, SHA-256 digests, UTF-8 byte sizes, public one-hour cache hints

DNS rebinding defense

Safe localhost default, configurable MCP_ALLOWED_HOSTS, exact-port and any-port entries; no CORS headers

SSRF defense for client URLs

Local/private IP literals, IPv4-mapped IPv6, and localhost names denied; redirects checked too

Incoming body cap

4 MiB, enforced while reading p3 chunks

Bounded outbound HTTP

Configurable full-exchange deadline and response cap, plus 4 MiB request cap

Streaming SSE

SDK SSE mode, progress notifications, 15-second keepalives, bounded response inactivity, peer cancellation

Concurrent native invocations

Tested overlapping outbound requests on one shared native instance, plus independent instances

Structured request/tool logs

JSON on stderr, level filtering, W3C trace correlation and outbound propagation

Optional host-joined native tracing

Separate mcp.otel.wasm importing wasi:otel/tracing@0.2.0-rc.2

Desktop integration

.wash/config.yaml, local and published Workload manifests, MCP catalog metadata, deny-all outbound defaults

Authorization guidance

docs/auth.md; both templates default to platform-boundary authorization and auth-type: none

Automated verification

Native protocol/parity fixtures, actual-production p3 boundary tests, native OTEL checks, and CI workflow

The remaining differences are runtime/toolchain and deployment validation: QuickJS needs the included compiler patch and Web API glue, the component still has standard non-HTTP p2 runtime imports, and live Cosmonic deployment and external DNS/TLS have not been tested in this sandbox. The optional OTEL build requires matching host WIT signatures, as does the Rust feature. This is feature parity for the tested template paths, not a conformance claim for every optional MCP feature or an assertion that the new CI workflow has run remotely.

Build

Prerequisites: Node.js 22.15+, npm, wasm-tools, and the patched QuickJS compiler. Composition also requires Rust 1.96+, wasm32-wasip2, and wac 0.12.0. Tests require Wasmtime 49.0.2. Production compilation uses JCO 1.37.0's bundler and a standalone patched componentize-qjs CLI. npm dependencies are locked.

npm ci
npm run build:compiler
npm run check
npm run build
npm run test:all

build:compiler checks out upstream componentize-qjs revision eee19a60b144321b2535a50cc58e05b73d9f5242, applies patches/componentize-qjs.patch, and builds it. This needs network access on first use. The compiler also downloads wasi-sdk 33 and Binaryen 130 unless cached or supplied through WASI_SDK_PATH and WASM_OPT. The patch comes from the local sandboxed-agents/harnesses/pi project; it supplies correctly typed async initialization stubs and real p2 HTTP resources during initialization. It does not add p2 HTTP imports here. The upstream MCP SDK is unmodified.

The compiler selection order is COMPONENTIZE_QJS, gen/componentize-qjs, then componentize-qjs on PATH. This workspace has a cached patched binary in gen/, which is excluded from source control. The release compiler's async-import initializer fails without the patch. JCO's default StarlingMonkey backend also fails to compile this p3 handler.

npm run build:otel       # also emits dist/mcp.otel.wasm
npm test                # production component + composed Rust test driver
npm run test:outbound   # outbound and parity fixtures, including streaming
npm run test:boundary   # independent production p3 invocation
npm run serve           # local Wasmtime verification listener

stat reports the exact size of dist/mcp.wasm; it is approximately 16.6 MB uncompressed, including the engine, SDK, polyfills, and embedded skills. Snapshot sizes vary slightly between builds. The optional OTEL component is separate.

Customize tools and skills

Edit src/server.ts using upstream McpServer, Zod, and tool callbacks. Its registration table also supplies discovery tool names. The SDK owns protocol validation, negotiation/discovery, routing, errors, schemas, and SSE framing. Keep requests stateless; durable state belongs in a host capability or an upstream service.

http_get performs one GET without following redirects, returning an HTTP status line and up to 4096 UTF-8 bytes, with a truncation marker. http_request supports methods, headers, text bodies, and follow/manual/error redirect policies. HTTP 404 and other non-2xx responses retain their status and body. Network, policy, size, and timeout failures become tool errors. Cross-origin redirects remove credentials. Progress is emitted when a caller supplies a progress token.

Write the operating playbook in skills/server/SKILL.md, with supporting files under that directory. Build-time YAML parsing preserves frontmatter in the manifest; name must equal package.json's name. Every UTF-8 file is embedded with its URI, digest, and byte size. No runtime filesystem lookup is performed. Rename the frontmatter and package together when forking.

The server advertises io.modelcontextprotocol/skills with directoryRead: true. Clients can use skills/list, skills/get, resources/directory/read, and resources/read under skill://mcp-off-shelf. The index resource and server instructions provide fallback discovery for clients without the extension. Supporting files are in the manifest and directory listings; only the playbook and index appear in resources/list. Unknown and traversal-shaped URIs fail.

Configuration and policy

Environment variable

Default

Meaning

MCP_ALLOWED_HOSTS

localhost, 127.0.0.1, ::1

Comma-separated incoming authorities; host accepts any port, host:port is exact, * disables the guard

MCP_HTTP_GET_ALLOW_LOCAL

unset

true or 1 permits local/private outbound targets for development; applies to both HTTP tools and redirects

MCP_OUTBOUND_TIMEOUT_MS

30000

Whole outbound exchange deadline, including body read

MCP_OUTBOUND_MAX_BYTES

4194304

Buffered outbound response limit

MCP_KEEPALIVE_MS

15000

SSE keepalive interval

MCP_RESPONSE_TIMEOUT_MS

60000

Response-stream inactivity deadline, including a peer that stops reading

MCP_LOG_LEVEL

info

trace/debug/info/warn/error/off stderr threshold

RUST_LOG

info

Compatibility fallback for a single global log level

The incoming protocol cap and outbound request cap are 4 MiB. Numeric settings must be positive safe integers no greater than 2147483647. Invalid configuration fails closed. RUST_LOG module-specific filter expressions are not interpreted; use MCP_LOG_LEVEL for this TypeScript server.

The in-guest SSRF check covers literal addresses and localhost spellings. The host must enforce DNS-resolved destinations and allowedHosts; a public-looking hostname can resolve to a private address. Both supplied manifests explicitly set allowedHosts: [], meaning deny-all. Add each required upstream there and in .wash/config.yaml. Neither network policy nor the Host guard authenticates a caller. See authorization guidance.

Discovery contains public server/tool/skill metadata, works under probe Host values, and does not read a request body. An ingress can gate it separately if that inventory is sensitive. No response enables cross-origin browser access.

Deploy on Cosmonic Desktop

Register this project using .wash/config.yaml, promote its build, then put the returned digest-pinned image into workload.yaml and apply through Desktop. Before applying, select an unused workload name, namespace, and ingress host so an existing deployment is not replaced. The local host is mcp-off-shelf.localhost; MCP_ALLOWED_HOSTS matches it.

For a public image, update the registry placeholder and host in deploy/workload.yaml. Both manifests bind the p3 handler, advertise current MCP/skills/stateless/streamable-HTTP metadata, and start with no outbound destinations. The same MCP catalog labels are declared in .wash/config.yaml so dev workloads and promoted drafts carry them too. The explicit manifests include desktop.cosmonic.com/source: mcp, matching the Rust template. Use host secretFrom references for API keys. The manifests describe deployment configuration; no workload is applied by a build or test command.

Desktop dev builds use node scripts/build.mjs --desktop and serve gen/desktop/mcp.wasm. All generated assets, WIT, Rust targets, and dev bundle outputs live under gen/, which Desktop's watcher ignores. The watcher does not read .gitignore; writing these files under generated/ or writing dev bundles under dist/ causes continuous rebuilds. Release builds still emit dist/mcp.wasm. Restart an existing dev session after this configuration change.

Run the local dev session in one terminal, then run the smoke test in another:

cosmonic dev --path .
npm run test:desktop

The smoke test connects to 127.0.0.1:8200 with the configured mcp-off-shelf.localhost Host header, avoiding wildcard localhost DNS. Override MCP_DESKTOP_URL and MCP_DESKTOP_HOST for another ingress address or binding. It checks both MCP versions, tools, skills and hashes, concurrent calls, local address rejection, and host-enforced denied egress to https://example.org/. Keep that destination outside the workload's allowlist; change the probe with MCP_DESKTOP_DENIED_URL if needed. A DNS or TLS failure does not count as a successful policy-denial check.

To also verify live DNS/TLS and SSE progress, grant only example.com in this project's workload.allowedHosts in .wash/config.yaml, restart its dev session, and run:

MCP_DESKTOP_ALLOWED_URL=https://example.com/ npm run test:desktop

Restore allowedHosts: [] after testing. The smoke script neither deploys nor changes Desktop policy. Native I/O cancellation and OTEL parentage are covered by the fixture suite; this smoke test does not verify Desktop cancellation or collected OTLP spans. This sandbox denies both the Desktop Unix socket and loopback ingress (EPERM), so the smoke test has not completed against Desktop.

CI

The CI workflow builds both component variants, checks TypeScript, runs protocol/parity/native-boundary/OTEL tests, checks Rust formatting, and lints the runtime adapter with Clippy. It caches npm downloads, Cargo dependencies and all three Rust build targets, plus the pinned component tools and patched compiler binary. Tool caches include OS, architecture, Rust version, tool versions, and compiler patch/build-script hashes. Rust and tool caches are saved only after successful pushes to main.

The zizmor workflow runs a blocking pedantic security audit. Actions are pinned to commit hashes, checkout credentials are not persisted, and workflows have read-only repository permissions. Locally:

zizmor --offline --pedantic --strict-collection .github/workflows
actionlint .github/workflows/*.yaml
npm run test:all

The workflows have passed local static checks; a clean hosted GitHub Actions run and cache restore remain to be verified after pushing this repository.

Observability

Request, tool, and outbound spans emit structured JSON to stderr. They carry trace/span IDs, parent IDs, durations, and request method/path without bodies, credentials, or query values. Valid W3C traceparent is accepted on ingress and propagated on outbound requests unless a tool explicitly provides that header. The platform owns collection and OTLP export.

npm run build:otel additionally emits a component using native wasi:otel/tracing@0.2.0-rc.2. It creates request/tool/client spans under the host's active context when no incoming parent is supplied. The native fixture checks span delivery and parentage. Ordinary Wasmtime serving has no OTEL host provider; use the standard artifact there. Hosts must match the vendored WIT signatures, especially tracing.on-start; the same nominal rc.2 version has had incompatible definitions in different host builds.

Verification and runtime notes

The test harness composes an independent Rust driver with the actual production artifact. The official TypeScript client drives its HTTP entry through native Wasmtime. Creating and consuming WASI resources on opposite component boundaries also exercises structured future payloads correctly. The test driver is separate from the Rust cancellation adapter included in the production component. Neither implements MCP.

Tests cover both MCP eras, skills manifests and directory traversal, discovery, Host policy, SSRF including redirects and mapped addresses, body limits, configurable timeouts and size caps, status codes, Unicode, streaming progress before completion, peer cancellation, response backpressure, and overlapping outbound calls on one native instance. The fixture uses p3 HTTP hooks with no p2 HTTP linker and no sockets. Native OTEL uses a separate composed artifact. CI installs the pinned compiler patch and runs the same checks.

src/runtime.ts and src/web.ts supply upstream Web Streams, AbortController, Fetch data classes, Buffer, URL, and secure UUIDs. src/timers.ts schedules SDK timers on p3 monotonic-clock waits; one shared short wait bounds retention after timers are cleared. src/future.ts handles QuickJS's explicit WIT future resources. The HTTP bridge buffers incoming messages within the cap and streams responses; outbound Fetch is buffered and policy checked. The composed HTTP adapter races each p3 exchange against an explicit cancellation signal, buffers the response within the limit, and drops native I/O when a deadline or disconnect cancels the operation. This is needed because QuickJS import promises cannot cancel their native subtask. The final component exposes only standard WASI host imports; the adapter's control interface is linked internally. JCO and ponyfill internal paths are pinned and need review during upgrades.

This is not a complete browser/Node runtime: automatic HTTP decompression and full Web Crypto are absent. Persistent subscriptions, sampling/elicitation, OAuth flows, and other optional MCP features beyond the Rust template's tested surface remain unverified. JCO-transpiled JavaScript hosts previously trapped on overlapping invocations; native Wasmtime shared-instance tests now pass. Live socket serving, external networking, and Desktop OTLP collection were not available for verification in this sandbox.

Sources

WASI p3 WIT comes from JCO 1.37.0. Optional OTEL and its p2 type dependencies come from opentelemetry-wasi revision 2962a286aa66188314bcafd79c800c4e39ce41e1, matching the Rust template's feature.

Related MCP Connectors

Related MCP Servers

  • -
    license
    B
    quality
    Not graded
    maintenance
    A boilerplate project for quickly developing Model Context Protocol (MCP) servers using TypeScript SDK. Includes example tools for calculations and greetings, plus system information resources.
    2
    3
    -
  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    A production-ready TypeScript MCP server providing basic tools (add, echo, timestamp), resources (server info, greetings, data access), and prompt templates (analyze, code-review, summarize). Serves as a foundation for building custom MCP servers with extensible architecture.
    237 npm
    -
  • A
    license
    B
    quality
    D
    maintenance
    A boilerplate project for quickly developing Model Context Protocol (MCP) servers using TypeScript SDK. Includes example tools like calculator and greeting functions, plus system information resources.
    3
    ISC
  • A
    license
    B
    quality
    D
    maintenance
    A demonstration TypeScript MCP server that showcases basic MCP concepts with simple tools (greeting, calculator), text resources, and prompt templates for learning the Model Context Protocol.
    2
    MIT