What is Wassette?

Overview

Wassette is a secure, open-source Model Context Protocol (MCP) server that leverages WebAssembly (Wasm) to provide a trusted execution environment for untrusted tools. MCP is a standard how LLMs access and share data with external tools. By embedding a WebAssembly runtime and applying fine-grained security policies, Wassette enables safe execution of third-party MCP tools without compromising the host system.

Key Features

Wassette provides the following key features:

  • Sandboxed tools using the WebAssembly Component Model
  • Fine-grained permissions for file system, network, and system resources
  • Developer-friendly approach that simplifies tool development by focusing on business logic rather than infrastructure complexity

Note: The name “Wassette” is a portmanteau of “Wasm” and “Cassette” (referring to magnetic tape storage), and is pronounced “Wass-ette”.

Problem Statement

The current landscape of MCP server deployment presents significant security challenges. Today’s common deployment patterns include standalone processes communicating via stdio or sockets, direct binary execution using package managers like npx or uvx, and container-based isolation providing basic security boundaries.

These approaches expose users to various security risks including unrestricted file system access where tools can read and write arbitrary files, network vulnerabilities through uncontrolled outbound connections to external services, code execution risks from malicious or vulnerable tools, and limited visibility making it difficult to monitor and audit tool behavior.

The fundamental issue is that current MCP servers run with the same privileges as the host process, creating an unacceptable attack surface for untrusted code execution.

Target Audience

Wassette serves four primary user groups:

  • Application Developers who want to focus on business logic implementation with reduced infrastructure complexity and simplified deployment
  • DevOps Engineers who benefit from platform-agnostic deployment capabilities, comprehensive observability and monitoring, and security-by-design architecture
  • End Users who gain a trusted execution environment for third-party tools with transparent security policies and protection against malicious or vulnerable tools
  • Platform Providers who can leverage Wassette’s serverless-ready architecture, consistent runtime environment, and scalable multi-tenant capabilities

Current Solutions Analysis

  1. Container-based isolation. This is perhaps the most common way to run MCP servers securely today, because it works with existing tooling and infrastructure and requires no changes to the server code. One could argue that containers are not a secure boundary, but they are a good starting point. The harder problem is how to apply security policies to the container like “how do I know what HTTP domain is this tool calling to?”. The Docker MCP Catalog runs each MCP server as a container - providing isolation and portability.
  2. Direct binary execution. Running binaries directly using npx or uvx. This is a simple way to run MCP servers (and often the default way MCP servers document how to use it), but it is not secure. It is easy to run a tool that has a vulnerability or malicious code that can read/write files on your machine, open sockets, or even execute arbitrary code.
  3. WebAssembly platforms. Centralized MCP server that runs WebAssembly-based tools locally (think tools like mcp.run). This has the advantage of running tools in tiny sandboxes which incur less memory overhead than containers. However, most of these tools still require custom ABIs and libraries and are not compatible with each other.

Wassette Solution

Design Philosophy

Wassette addresses the security and interoperability challenges of current MCP deployments by leveraging the WebAssembly Component Model. This approach provides strong security boundaries through WebAssembly’s sandboxed execution environment, capability-based access control with fine-grained permission management, tool interoperability via standardized component interfaces, transparent security through explicit capability declarations, and low resource overhead with efficient memory usage compared to containers.

Architecture Goals

Wassette implements a centralized trusted computing base (TCB) through a single, open-source MCP server implementation built with memory-safe, high-performance runtimes like Wasmtime, maintaining a minimal attack surface through reduced complexity.

The system enforces capability-based security with allow/deny lists for file system paths, network endpoint access control, system call restrictions, and a policy engine similar to policy-mcp-rs.

For secure distribution, WebAssembly components are distributed as OCI artifacts with cryptographic signature verification, registry-based tool distribution, and granular capability declarations per tool.

Example Permission Policy

version: "1.0"
description: "An example policy"
permissions:
  storage:
    allow:
    - uri: "fs://workspace/**"
      access: ["read", "write"]
    - uri: "fs://config/app.yaml"
      access: ["read"]
  network:
    allow:
    - host: "api.openai.com"

Developer Experience

Developers will write MCP tools as functions that can be compiled to WebAssembly Components, instead of developing servers. This is a significant paradigm shift and offers a completely different experience than writing MCP servers as it currently stands. We are fully aware that current MCP server code would need to be rewritten for retargeting to Wasm but the security benefits and flexibility of the Component Model are worth it.

We are exploring AI tools that make porting existing MCP servers to Wasm easier, removing the biggest barrier to adoption.

Language Support

Wassette supports tools written in any language that can compile to WebAssembly Components. For current language support, see the WebAssembly Language Support Guide.

Wassette provides examples in JavaScript and Python, which are the most popular languages for MCP server development; explore the examples.

Installation

Wassette is available for Linux, macOS, and Windows. Choose the installation method that best suits your platform and workflow.

Quick Start

For the fastest installation experience, we recommend:

  • Linux/macOS: Use our one-liner install script
  • macOS: Use Homebrew
  • Windows: Use WinGet
  • Nix users: Use Nix flakes

Installation by Platform

The easiest way to install Wassette on Linux is using our automated install script:

curl -fsSL https://raw.githubusercontent.com/microsoft/wassette/main/install.sh | bash

This script will:

  • Automatically detect your system architecture (x86_64 or ARM64)
  • Download the latest Wassette release
  • Install the binary to ~/.local/bin
  • Configure your shell PATH for immediate access

Homebrew

If you prefer using Homebrew on Linux:

brew tap microsoft/wassette https://github.com/microsoft/wassette
brew install wassette

Manual Download

You can also download the latest Linux release manually from the GitHub Releases page and add it to your $PATH.

Verifying the Installation

After installation, verify that Wassette is properly installed and accessible:

wassette --version

This should display the installed version of Wassette.

Supported Platforms

Wassette supports the following platforms:

Operating SystemArchitectureSupport
Linuxx86_64 (amd64)✅ Full support
LinuxARM64 (aarch64)✅ Full support
macOSIntel (x86_64)✅ Full support
macOSApple Silicon (ARM64)✅ Full support
Windowsx86_64✅ Full support
WindowsARM64✅ Full support
Windows Subsystem for Linuxx86_64, ARM64✅ Full support

Next Steps

Once Wassette is installed, you’ll need to configure it with your AI agent:

  1. Configure with your AI agent: Follow the MCP clients setup guide for instructions on integrating Wassette with:

    • Visual Studio Code
    • Cursor
    • Claude Code
    • Gemini CLI
  2. Load your first component: Try loading a sample component to verify everything works:

    Please load the time component from oci://ghcr.io/yoshuawuyts/time:latest
    
  3. Explore examples: Check out the examples directory for sample components in different languages.

Troubleshooting

Command not found

If you get a “command not found” error after installation:

  • Linux/macOS: Ensure ~/.local/bin is in your PATH. You may need to restart your terminal or run:

    export PATH="$HOME/.local/bin:$PATH"
    
  • Windows: Ensure the installation directory is in your system PATH. You may need to restart your terminal or log out and back in.

Permission denied

If you encounter permission errors:

  • Linux/macOS: Ensure the binary has execute permissions:

    chmod +x ~/.local/bin/wassette
    
  • Windows: Run PowerShell as Administrator when installing with WinGet.

Other Issues

For additional help:

  • Check the FAQ for common questions and answers
  • Visit our GitHub Issues page
  • Join our community discussions

Upgrading

To upgrade to the latest version of Wassette:

  • Homebrew: brew update && brew upgrade wassette
  • WinGet: winget upgrade Wassette
  • Install script: Re-run the install script
  • Nix: nix profile upgrade github:microsoft/wassette
  • Manual: Download the latest release and replace your existing binary

Model Context Protocol (MCP) Clients

If you haven’t installed Wassette yet, follow the installation instructions first.

Visual Studio Code

Add the Wassette MCP Server to GitHub Copilot in Visual Studio Code by clicking the Install in VS Code or Install in VS Code Insiders badge below:

Install in VS Code Install in VS Code Insiders

Alternatively, you can add the Wassete MCP server to VS Code from the command line using the code command in a bash/zsh or PowerShell terminal:

bash/zsh

code --add-mcp '{"name":"Wassette","command":"wassette","args":["serve","--stdio"]}'

PowerShell

 code --% --add-mcp "{\"name\":\"wassette\",\"command\":\"wassette\",\"args\":[\"serve\",\"--stdio\"]}"

You can list and configure MCP servers in VS Code by running the command MCP: List Servers in the command palette (Ctrl+Shift+P or Cmd+Shift+P).

Cursor

Click the below button to use the one-click installation to add Wassette to Cursor.

Install MCP Server

Claude Code

First, install Claude Code (requires Node.js 18 or higher):

npm install -g @anthropic-ai/claude-code

Add the Wassette MCP server to Claude Code using the following command:

claude mcp add -- wassette wassette serve --stdio

This will configure the Wassette MCP server as a local stdio server that Claude Code can use to execute Wassette commands and interact with your data infrastructure.

You can verify the installation by running:

claude mcp list

To remove the server if needed:

claude mcp remove wassette

Gemini CLI

First, install Gemini CLI (requires Node.js 20 or higher):

npm install -g @google/gemini-cli

To add the Wassette MCP server to Gemini CLI, you need to configure it in your settings file at ~/.gemini/settings.json. Create or edit this file to include:

{
  "mcpServers": {
    "wassette": {
      "command": "wassette",
      "args": ["serve", "--stdio"]
    }
  }
}

Quit the Gemini CLI and reopen it.

Open Gemini CLI and verify the installation by running /mcp inside of Gemini CLI.

OpenAI Codex CLI

First, install Codex CLI (requires Node.js) using either npm or Homebrew:

npm install -g @openai/codex

Or with Homebrew:

brew install codex

Add the Wassette MCP server to Codex CLI using the following command:

codex mcp add wassette wassette serve --stdio

Run codex to start the CLI.

Verify the installation by running /mcp inside of Codex CLI.

Frequently Asked Questions (FAQ)

General Questions

What is Wassette?

Wassette is a secure, open-source Model Context Protocol (MCP) server that leverages WebAssembly (Wasm) to provide a trusted execution environment for untrusted tools. It enables safe execution of third-party MCP tools without compromising the host system by using WebAssembly’s sandboxed execution environment and fine-grained security policies.

Note: The name “Wassette” is a portmanteau of “Wasm” and “Cassette” (referring to magnetic tape storage), and is pronounced “Wass-ette”.

Is Wassette a MCP server?

Yes, Wassette is itself a local MCP server.

How is Wassette different from other MCP servers?

Traditional MCP servers run with the same privileges as the host process, creating security risks. Wassette addresses this by:

  • Sandboxed execution: Tools run in WebAssembly’s secure sandbox, not directly on the host
  • Fine-grained permissions: Explicit control over file system, network, and system resource access
  • Component-based architecture: Uses the standardized WebAssembly Component Model for tool interoperability
  • Centralized security: Single trusted computing base instead of multiple potentially vulnerable servers

What are WebAssembly Components?

WebAssembly Components are a standardized way to build portable, secure, and interoperable software modules. Unlike traditional WebAssembly modules, Components use the WebAssembly Component Model which provides:

  • Standardized interfaces defined using WebAssembly Interface Types (WIT)
  • Language interoperability - components can be written in any language that compiles to Wasm
  • Composability - components can be combined and reused across different environments

Language and Development

What programming languages are supported?

Wassette supports tools written in any language that can compile to WebAssembly Components. For current language support, see the WebAssembly Language Support Guide.

The project includes examples in several popular languages:

Can I use existing WebAssembly modules with Wassette?

Wassette specifically requires WebAssembly Components (not just modules) that follow the Component Model. Existing Wasm modules would need to be adapted to use the Component Model’s interface system.

How do I create a Wasm component?

  1. Define your interface using WebAssembly Interface Types (WIT)
  2. Implement the functionality in your preferred supported language
  3. Compile to a Component using appropriate tooling for your language
  4. Test with Wassette by loading the component

See the examples directory for complete working examples in different languages.

Do I need to rewrite existing MCP servers?

Yes, existing MCP servers would need to be rewritten to target wasip2 (WebAssembly Components). This is a significant paradigm shift from writing servers to writing functions that compile to Wasm Components. However, the security benefits and flexibility of the Component Model make this worthwhile.

The project is exploring AI tools to help port existing MCP servers to Wasm, which should reduce the migration effort.

Security and Permissions

How does Wassette’s security model work?

Wassette implements a capability-based security model with:

  • Sandbox isolation: All tools run in WebAssembly’s secure sandbox
  • Explicit permissions: Components must declare what resources they need access to
  • Allow/deny lists: Fine-grained control over file system paths, network endpoints, etc.
  • Principle of least privilege: Components only get the permissions they explicitly need

Compared to running tools directly with an MCP SDK, Wassette enforces sandboxing and permissions at runtime. This prevents tools from inheriting host-level privileges and reduces the risk of data exfiltration or privilege escalation.

What is a policy file?

A policy file (policy.yaml) defines what permissions a component has. Example:

version: "1.0"
description: "Permission policy for filesystem access"
permissions:
  storage:
    allow:
      - uri: "fs://workspace/**"
        access: ["read", "write"]
      - uri: "fs://config/app.yaml"
        access: ["read"]
  network:
    allow:
      - host: "api.openai.com"

This policy permits read/write access to a workspace directory, read-only access to a specific config file, and network egress only to api.openai.com. All other filesystem and network access is denied and will be blcoked by the sandbox.

Can I grant permissions at runtime?

Yes, Wassette provides built-in tools for dynamic permission management:

  • grant-storage-permission: Grant file system access
  • grant-network-permission: Grant network access
  • grant-environment-variable-permission: Grant environment variable access

You can also revoke previously granted permissions with the corresponding revoke-* tools.

What happens if a component tries to access unauthorized resources?

The WebAssembly sandbox will block the access attempt. Wassette enforces permissions at the runtime level, so unauthorized access attempts are prevented rather than just logged.

Why not just use the Python or TypeScript SDK to build a server?

You can and many developers do. SDKs let you register and run tools directly from server code.

The difference is how tools execute:

  • SDKs only: Tools run with the same privileges as the host process
  • SDKs + Wassette: Each tool runs in an isolated sandbox with deny-by-default, auditable permissions

Wassette is especially valuable in enterprise or multii-tenant environments, or when running untrusted/community tools, where stronger runtime safeguards are required.

Installation and Setup

What platforms does Wassette support?

Wassette supports:

  • Linux (including Windows Subsystem for Linux)
  • macOS
  • Windows (via WinGet package)

How do I install Wassette?

See the Installation guide for complete instructions for all platforms including:

  • Linux/macOS one-liner install script
  • Homebrew for macOS and Linux
  • WinGet for Windows
  • Nix flakes for reproducible environments

How do I configure Wassette with my AI agent?

Wassette works with any MCP-compatible AI agent. See the MCP clients setup guide for specific instructions for:

  • Visual Studio Code
  • Cursor
  • Claude Code
  • Gemini CLI

Usage and Troubleshooting

How do I load a component in Wassette?

You can load components from OCI registries or local files:

Please load the component from oci://ghcr.io/microsoft/time-server-js:latest

Or for local files:

Please load the component from ./path/to/component.wasm

What built-in tools does Wassette provide?

Wassette includes several built-in management tools:

  • load-component: Load WebAssembly components
  • unload-component: Unload components
  • list-components: List loaded components
  • get-policy: Get policy information
  • grant-storage-permission: Grant storage access
  • grant-network-permission: Grant network access
  • grant-environment-variable-permission: Grant environment variable access
  • revoke-storage-permission: Revoke storage access permissions
  • revoke-network-permission: Revoke network access permissions
  • revoke-environment-variable-permission: Revoke environment variable access permissions
  • reset-permission: Reset all permissions for a component

What’s a practical use case?

One example is the fetch tool. With Wassette, you can write a policy that restricts the tool to only contact a specific API endpoint, such as weather.com. This means that even if the tool is compromised, it cannot exfiltrate data from your internal APIs or file systems. It is strictly limited to the network host you approved.

This makes it safe to:

  • Run untrusted or community-contributed tools.
  • Allow third-party extensions in enterprise environments without exposing sensitive systems.
  • Confidently deploy MCP agents in multi-tenant or regulated environments.

How do I debug component issues?

  1. Check the logs: Run Wassette with RUST_LOG=debug for detailed logging
  2. Verify permissions: Ensure your policy file grants necessary permissions
  3. Test component separately: Validate that your component works outside Wassette
  4. Check the interface: Ensure your WIT interface matches what Wassette expects

Are there performance implications of using WebAssembly?

WebAssembly Components in Wassette have:

  • Lower memory overhead compared to containers
  • Fast startup times due to efficient Wasm instantiation
  • Near-native performance for CPU-intensive tasks
  • Minimal runtime overhead thanks to Wasmtime’s optimizations

Can I use Wassette in production?

Wassette is actively developed and used by Microsoft. However, as with any software, you should:

  • Test thoroughly in your specific environment
  • Review the security model for your use case
  • Keep up with updates and security patches
  • Consider your specific requirements for stability and support

Getting Help

Where can I get support?

How can I contribute to Wassette?

See the Contributing Guide for information on:

  • Setting up the development environment
  • Submitting bug reports and feature requests
  • Contributing code and documentation
  • Following the project’s coding standards

Where can I find more examples?

The examples directory contains working examples in multiple languages:

Managing Permissions

Wassette uses a fine-grained permission system to control what resources WebAssembly components can access. This page explains how to work with permissions in your day-to-day use of Wassette.

Overview

Every component in Wassette runs in a secure sandbox with deny-by-default permissions. This means:

  • No access by default: Components cannot access files, networks, or environment variables unless explicitly granted
  • Per-component policies: Each component has its own independent permission set
  • Runtime enforcement: The WebAssembly sandbox blocks unauthorized access attempts

Permission Types

Wassette supports four types of permissions:

Storage Permissions

Control file system access for reading and writing files.

Example uses:

  • Allow a component to read configuration files
  • Grant write access to output directories
  • Restrict access to specific workspace folders

Network Permissions

Control outbound network access to specific hosts.

Example uses:

  • Allow API calls to external services
  • Permit access to specific domains only
  • Restrict network egress for security

Environment Variable Permissions

Control access to environment variables.

Example uses:

  • Provide API keys to components
  • Share configuration via environment
  • Control access to sensitive credentials

Memory Permissions

Set memory limits for components (future capability).

Example uses:

  • Prevent resource exhaustion
  • Enforce quotas in multi-tenant environments

Granting Permissions

The recommended way to grant permissions is through your AI agent when running Wassette as an MCP server. You can also use CLI commands for direct management, or define permissions in policy files.

When running Wassette as an MCP server, simply ask your AI agent to grant permissions in natural language:

Please grant storage read and write permissions to the weather-tool for fs://workspace/

The agent will automatically use the appropriate built-in tool to apply the permission.

More examples:

Grant network access to api.weather.com for the weather-tool component
Allow the weather-tool to access the API_KEY environment variable

Available MCP tools:

  • grant-storage-permission: Grant file system access
  • grant-network-permission: Grant network access
  • grant-environment-variable-permission: Grant environment variable access

The agent understands permission requests and selects the right tool, so you don’t need to worry about command syntax.

Note: After granting environment variable permissions, the server must be able to see those environment variables. You can provide them by:

  • Using wassette secret set <component-id> <key> <value> to inject secrets
  • Running the server with the necessary environment variables already set

Using CLI Commands

For direct management or scripting, use the wassette permission grant command:

Grant storage access:

# Read-only access to a directory
wassette permission grant storage weather-tool fs://workspace/ --access read

# Read and write access
wassette permission grant storage weather-tool fs://workspace/ --access read,write

# Access to a specific file
wassette permission grant storage weather-tool fs://config/app.yaml --access read

Grant network access:

# Allow access to a specific host
wassette permission grant network weather-tool api.weather.com

# Allow localhost access
wassette permission grant network weather-tool localhost:8080

Grant environment variable access:

# Grant access to an environment variable
wassette permission grant environment-variable weather-tool API_KEY

# Grant access to multiple variables
wassette permission grant environment-variable weather-tool HOME
wassette permission grant environment-variable weather-tool PATH

Using Policy Files

Policy files store permissions for components in YAML format. These files are typically managed automatically by Wassette when you use the built-in tools or CLI commands rather than being manually written.

When you grant permissions through MCP built-in tools or CLI commands, Wassette creates and updates a policy.yaml file alongside your component:

version: "1.0"
description: "Weather tool permissions"
permissions:
  storage:
    allow:
      - uri: "fs://workspace/**"
        access: ["read", "write"]
      - uri: "fs://config/app.yaml"
        access: ["read"]
  network:
    allow:
      - host: "api.weather.com"
      - host: "api.openweathermap.org"
  environment:
    allow:
      - key: "API_KEY"
      - key: "WEATHER_API_TOKEN"

Policy file structure:

  • version: Policy format version (currently “1.0”)
  • description: Human-readable description
  • permissions: Permission declarations organized by type
    • storage.allow: List of file system URIs and access types
    • network.allow: List of allowed hosts
    • environment.allow: List of environment variable keys

While you can manually create or edit policy files for distributing components with predefined permissions, for most use cases, granting permissions through the AI agent or CLI commands is simpler and less error-prone.

Revoking Permissions

Remove previously granted permissions using the wassette permission revoke command:

Revoke storage access:

wassette permission revoke storage weather-tool fs://workspace/

Revoke network access:

wassette permission revoke network weather-tool api.weather.com

Revoke environment variable access:

wassette permission revoke environment-variable weather-tool API_KEY

Reset All Permissions

To remove all permissions for a component:

wassette permission reset weather-tool

This returns the component to its default deny-all state.

Checking Permissions

View the current permissions for a component:

Using CLI:

# Get policy in JSON format
wassette policy get weather-tool

# Get policy in YAML format
wassette policy get weather-tool --output-format yaml

Using MCP:

What are the current permissions for weather-tool?

The agent will use the get-policy tool to retrieve the information.

Common Permission Patterns

Development Environment

Grant broad permissions for local development:

version: "1.0"
description: "Development permissions"
permissions:
  storage:
    allow:
      - uri: "fs://$(pwd)/workspace/**"
        access: ["read", "write"]
      - uri: "fs://$(pwd)/config/**"
        access: ["read"]
  network:
    allow:
      - host: "localhost"
      - host: "127.0.0.1"
      - host: "*.local"
  environment:
    allow:
      - key: "HOME"
      - key: "USER"
      - key: "PWD"

Production Environment

Restrict permissions to minimum required:

version: "1.0"
description: "Production permissions"
permissions:
  storage:
    allow:
      - uri: "fs:///app/data/**"
        access: ["read"]
      - uri: "fs:///app/cache/**"
        access: ["read", "write"]
  network:
    allow:
      - host: "api.production-service.com"
  environment:
    allow:
      - key: "API_KEY"

Untrusted Components

Minimal permissions for third-party components:

version: "1.0"
description: "Restricted third-party component"
permissions:
  storage:
    allow:
      - uri: "fs:///tmp/component-cache/**"
        access: ["read", "write"]
  network:
    allow:
      - host: "api.trusted-vendor.com"
  # No environment variable access

Security Best Practices

Principle of Least Privilege

Only grant the minimum permissions needed:

Good:

permissions:
  storage:
    allow:
      - uri: "fs:///app/config/settings.yaml"
        access: ["read"]

Too permissive:

permissions:
  storage:
    allow:
      - uri: "fs:///**"
        access: ["read", "write"]

Use Specific Paths

Avoid wildcards when possible:

Good:

permissions:
  network:
    allow:
      - host: "api.example.com"
      - host: "cdn.example.com"

Too broad:

permissions:
  network:
    allow:
      - host: "*.example.com"

Audit Regularly

Review component permissions periodically:

# List all components
wassette component list

# Check permissions for each
for component in $(wassette component list | jq -r '.components[].id'); do
  echo "=== $component ==="
  wassette policy get $component --output-format yaml
done

Test Before Production

Validate permissions in a safe environment:

  1. Load component in test environment
  2. Grant minimal permissions
  3. Test functionality
  4. Add permissions incrementally as needed
  5. Document final permission set

Troubleshooting

Component Cannot Access Files

Symptom: Component fails when trying to read or write files.

Solution:

  1. Check current permissions: wassette policy get <component-id>
  2. Verify the file path matches the policy URI
  3. Ensure access level includes required operations (read/write)
  4. Grant missing permissions: wassette permission grant storage <component-id> fs://path --access read,write

Network Requests Failing

Symptom: Component cannot make network requests.

Solution:

  1. Check current permissions: wassette policy get <component-id>
  2. Verify the host is in the allow list
  3. Check for typos in host names
  4. Grant missing permissions: wassette permission grant network <component-id> api.example.com

Environment Variables Not Available

Symptom: Component cannot read environment variables.

Solution:

  1. Check current permissions: wassette policy get <component-id>
  2. Verify the variable key is in the allow list
  3. Ensure the environment variable is set in your shell
  4. Grant missing permissions: wassette permission grant environment-variable <component-id> VAR_NAME

Permission Changes Not Taking Effect

Solution:

  • Restart the Wassette server after modifying policy files
  • Use runtime permission commands for immediate effect
  • Verify changes with wassette policy get <component-id>

What Happens When Access is Denied?

When a component tries to access an unauthorized resource:

  1. The WebAssembly sandbox blocks the attempt - No unauthorized access occurs
  2. The operation fails - The component receives an error
  3. No security exceptions are raised - This is expected behavior
  4. Logs record the attempt - Check logs with RUST_LOG=debug

This deny-by-default behavior ensures components cannot exceed their granted capabilities.

Next Steps

Additional Resources

Cookbook: Building Wasm Components for Wassette

Welcome to the Wassette Cookbook! This section provides practical guides and recipes for building WebAssembly (Wasm) components that work with Wassette from various programming languages.

What You’ll Learn

The cookbook guides will walk you through:

  • Setting up your development environment for each language
  • Understanding WebAssembly Interface Types (WIT)
  • Creating component interfaces
  • Implementing component logic
  • Building and testing your components
  • Best practices and common patterns

Available Language Guides

Choose the programming language you want to use to build your Wasm component:

JavaScript/TypeScript

Build Wasm components using JavaScript or TypeScript with the Bytecode Alliance’s jco tooling. Perfect for developers familiar with Node.js ecosystem.

Key highlights:

  • Use familiar JavaScript/TypeScript syntax
  • Leverage npm packages and existing JavaScript libraries
  • Quick build times with jco componentize
  • Examples: time server, weather API, data processing

Python

Create Wasm components using Python with componentize-py. Ideal for data processing, scripting, and AI/ML workflows.

Key highlights:

  • Write components in pure Python
  • Use the uv package manager for fast builds
  • Access Python’s rich ecosystem
  • Examples: calculator, code execution, data analysis

Rust

Build high-performance Wasm components with Rust. Best for performance-critical tools and system-level programming.

Key highlights:

  • Near-native performance
  • Strong type safety and memory safety
  • Extensive WebAssembly tooling support
  • Examples: file system operations, HTTP clients

Go

Develop Wasm components using Go and TinyGo. Great for developers who prefer Go’s simplicity and concurrency features.

Key highlights:

  • Familiar Go syntax and idioms
  • Good performance characteristics
  • Growing WebAssembly support
  • Examples: module information service

Getting Started

If you’re new to WebAssembly components, we recommend:

  1. Start with the language you know best - Each guide is self-contained and provides all the necessary context
  2. Review the Architecture documentation - Understand how Wassette works with Wasm components
  3. Check out the Examples - See working implementations in action
  4. Read the FAQ - Find answers to common questions

Prerequisites

All guides assume basic familiarity with:

  • Command-line tools and terminals
  • Your chosen programming language
  • Basic WebAssembly concepts (though we explain them in each guide)

Common Concepts Across All Languages

Regardless of which language you choose, you’ll work with:

WIT (WebAssembly Interface Types)

WIT is an Interface Definition Language (IDL) that defines how your component interacts with Wassette and other systems. All guides show you how to write WIT interfaces.

Example WIT interface:

package local:my-tool;

world my-component {
    export process: func(input: string) -> result<string, string>;
}

Component Model

The WebAssembly Component Model provides a standard way to create portable, composable, and secure modules. Your components will follow this model regardless of the source language.

WASI (WebAssembly System Interface)

WASI provides a standard interface for WebAssembly components to access system capabilities like file I/O, networking, and random number generation. Each guide explains which WASI features are available.

Testing Your Components

Once you’ve built a component, you can test it with Wassette:

# Load and test your component
wassette serve --sse --plugin-dir /path/to/your/component

# Or load it explicitly
wassette load file:///path/to/your/component.wasm

For more details on testing, see the individual language guides.

Contributing Your Components

Have you built a useful component? Consider contributing it to the Wassette examples! See our Contributing Guide for details.

Next Steps

Pick a language guide above and start building your first Wasm component! Each guide provides step-by-step instructions with working examples.

Additional Resources

Building Wasm Components with JavaScript/TypeScript

This cookbook guide shows you how to build WebAssembly components using JavaScript or TypeScript that work with Wassette.

Quick Start

Prerequisites

  • Node.js (version 18 or later)
  • npm or yarn package manager

Install Tools

npm install -g @bytecodealliance/jco

Step-by-Step Guide

1. Create Your Project

mkdir my-component
cd my-component
npm init -y

2. Install Dependencies

{
  "type": "module",
  "dependencies": {
    "@bytecodealliance/componentize-js": "^0.18.1",
    "@bytecodealliance/jco": "^1.11.1"
  },
  "scripts": {
    "build:component": "jco componentize -w ./wit main.js -o component.wasm"
  }
}
npm install

3. Define Your Interface (WIT)

Create wit/world.wit:

package local:my-component;

interface calculator {
    add: func(a: s32, b: s32) -> s32;
    divide: func(a: f64, b: f64) -> result<f64, string>;
}

world calculator-component {
    export calculator;
}

4. Implement Your Component

Create main.js:

export const calculator = {
    add(a, b) {
        return a + b;
    },
    
    divide(a, b) {
        if (b === 0) {
            return { tag: "err", val: "Division by zero" };
        }
        return { tag: "ok", val: a / b };
    }
};

5. Build Your Component

# Basic build
jco componentize main.js --wit ./wit -o component.wasm

# Build with WASI dependencies
jco componentize main.js --wit ./wit -d http -d random -d stdio -o component.wasm

Common WASI dependencies:

  • http - HTTP client capabilities
  • random - Random number generation
  • stdio - Standard input/output
  • filesystem - File system access
  • clocks - Time and clock access

6. Test Your Component

wassette serve --sse --plugin-dir .

Complete Examples

Simple Time Server

wit/world.wit:

package local:time-server;

world time-server {
    export get-current-time: func() -> string;
}

main.js:

export function getCurrentTime() {
    return new Date().toISOString();
}

HTTP Weather Service

wit/world.wit:

package local:weather;

world weather-service {
    export get-weather: func(location: string) -> result<string, string>;
}

main.js:

import { fetch } from 'wasi:http/outgoing-handler';

export async function getWeather(location) {
    try {
        const response = await fetch(`https://api.weather.com/${location}`);
        const data = await response.json();
        return { tag: "ok", val: JSON.stringify(data) };
    } catch (error) {
        return { tag: "err", val: error.message };
    }
}

Error Handling

JavaScript components use WIT’s result type for error handling:

// Success
return { tag: "ok", val: resultValue };

// Error
return { tag: "err", val: "Error message" };

Using WASI Interfaces

HTTP Client

import { fetch } from 'wasi:http/outgoing-handler';

const response = await fetch('https://api.example.com/data');

Random Numbers

import { getRandomBytes } from 'wasi:random/random';

const bytes = getRandomBytes(16);

File System

import { read, write } from 'wasi:filesystem/types';

const content = await read('/path/to/file');
await write('/path/to/file', data);

Best Practices

  1. Use clear interface definitions - Make your WIT interfaces descriptive and well-documented
  2. Handle errors properly - Always use result<T, string> for operations that can fail
  3. Keep components focused - Each component should do one thing well
  4. Test thoroughly - Validate your component works before deploying
  5. Document your interfaces - Use WIT comments to explain your API

Common Patterns

Async Operations

export async function processData(input) {
    const result = await fetchExternalData(input);
    return result;
}

Type Conversions

// WIT types map to JavaScript as follows:
// s32, s64, u32, u64 -> Number
// f32, f64 -> Number
// string -> String
// bool -> Boolean
// list<T> -> Array
// record -> Object

Configuration

export const config = {
    timeout: 5000,
    retries: 3
};

Troubleshooting

Build Errors

  • Ensure Node.js version is 18 or later
  • Check that WIT interface matches your exports
  • Verify all dependencies are installed

Runtime Errors

  • Check WASI permission configuration
  • Validate input/output types match WIT interface
  • Review Wassette logs for details

Full Documentation

For complete details, including advanced topics, WASI interfaces, and more examples, see the JavaScript/TypeScript Development Guide.

Working Examples

See these complete working examples in the repository:

Next Steps

Building Wasm Components with Python

This cookbook guide shows you how to build WebAssembly components using Python that work with Wassette.

Quick Start

Prerequisites

  • Python 3.10 or higher
  • uv - Fast Python package manager

Install Tools

# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install componentize-py
uv pip install componentize-py

Step-by-Step Guide

1. Create Your Project

mkdir my-python-tool
cd my-python-tool
mkdir wit wit_world

2. Define Your Interface (WIT)

Create wit/world.wit:

package local:my-tool;

/// Example calculator tool
world calculator {
    /// Add two numbers and return the result
    export add: func(a: f64, b: f64) -> result<f64, string>;
    
    /// Perform a calculation from a string expression
    export calculate: func(expression: string) -> result<string, string>;
}

3. Generate Python Bindings

uv run componentize-py -d wit -w calculator bindings .

This creates Python bindings in the wit_world/ directory.

4. Implement Your Component

Create main.py:

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import wit_world
from wit_world.types import Err
import json

def handle_error(e: Exception) -> Err[str]:
    """Helper function to convert Python exceptions to WIT errors"""
    message = str(e)
    if message == "":
        return Err(f"{type(e).__name__}")
    else:
        return Err(f"{type(e).__name__}: {message}")

class Calculator(wit_world.Calculator):
    def add(self, a: float, b: float) -> float:
        """Add two numbers together"""
        try:
            result = a + b
            return result
        except Exception as e:
            raise handle_error(e)
    
    def calculate(self, expression: str) -> str:
        """Evaluate a mathematical expression and return JSON result"""
        try:
            # WARNING: eval() is unsafe for untrusted input
            # In production, use ast.literal_eval() or a proper expression parser
            result = eval(expression)
            return json.dumps({"result": result})
        except Exception as e:
            raise handle_error(e)

5. Create Build Configuration

Create Justfile:

install-uv:
    if ! command -v uv &> /dev/null; then curl -LsSf https://astral.sh/uv/install.sh | sh; fi

install: install-uv
    uv pip install componentize-py

bindings:
    uv run componentize-py -d wit -w calculator bindings .

build:
    uv run componentize-py -d wit -w calculator componentize -s main -o calculator.wasm

all: bindings build

6. Build Your Component

# Install build tools
just install

# Generate bindings and build Wasm component
just all

# Or run commands manually:
# uv run componentize-py -d wit -w calculator bindings .
# uv run componentize-py -d wit -w calculator componentize -s main -o calculator.wasm

7. Test Your Component

wassette serve --sse --plugin-dir .

Complete Examples

Simple Calculator

wit/world.wit:

package local:calculator;

world calculator {
    export add: func(a: f64, b: f64) -> f64;
    export subtract: func(a: f64, b: f64) -> f64;
    export multiply: func(a: f64, b: f64) -> f64;
    export divide: func(a: f64, b: f64) -> result<f64, string>;
}

main.py:

import wit_world
from wit_world.types import Err, Ok

class Calculator(wit_world.Calculator):
    def add(self, a: float, b: float) -> float:
        return a + b
    
    def subtract(self, a: float, b: float) -> float:
        return a - b
    
    def multiply(self, a: float, b: float) -> float:
        return a * b
    
    def divide(self, a: float, b: float):
        if b == 0:
            return Err("Division by zero")
        return Ok(a / b)

Data Processing Tool

wit/world.wit:

package local:data-processor;

world processor {
    export process-csv: func(data: string) -> result<string, string>;
    export analyze-data: func(data: string) -> result<string, string>;
}

main.py:

import wit_world
from wit_world.types import Ok, Err
import csv
import json
from io import StringIO

class Processor(wit_world.Processor):
    def process_csv(self, data: str) -> str:
        try:
            reader = csv.DictReader(StringIO(data))
            rows = list(reader)
            return Ok(json.dumps(rows))
        except Exception as e:
            return Err(f"CSV processing error: {str(e)}")
    
    def analyze_data(self, data: str) -> str:
        try:
            items = json.loads(data)
            analysis = {
                "count": len(items),
                "summary": f"Processed {len(items)} items"
            }
            return Ok(json.dumps(analysis))
        except Exception as e:
            return Err(f"Analysis error: {str(e)}")

Error Handling

Python components use WIT’s result type for error handling:

from wit_world.types import Ok, Err

# Success
return Ok(result_value)

# Error
return Err("Error message")

# Or raise an exception
raise handle_error(exception)

Working with WIT Types

Type Mappings

# WIT type -> Python type
# s32, s64, u32, u64 -> int
# f32, f64 -> float
# string -> str
# bool -> bool
# list<T> -> List[T]
# option<T> -> Optional[T]
# result<T, E> -> Ok[T] | Err[E]
# record -> dataclass or dict

Complex Types

from dataclasses import dataclass
from typing import Optional, List

@dataclass
class Person:
    name: str
    age: int
    email: Optional[str]

def process_people(people: List[Person]) -> str:
    return json.dumps([p.__dict__ for p in people])

Best Practices

  1. Use type hints - Python type hints help catch errors early
  2. Handle errors properly - Always return Ok or Err for result types
  3. Document your code - Use docstrings to explain functionality
  4. Test thoroughly - Validate edge cases and error conditions
  5. Keep it simple - Avoid complex dependencies that might not work in Wasm
  6. Avoid eval() for untrusted input - Use ast.literal_eval() or proper parsers instead of eval() to prevent code injection

Common Patterns

JSON Processing

import json

def process_json(data: str) -> str:
    try:
        parsed = json.loads(data)
        # Process data
        result = {"processed": True, "data": parsed}
        return Ok(json.dumps(result))
    except json.JSONDecodeError as e:
        return Err(f"Invalid JSON: {str(e)}")

File Processing

def read_file(path: str) -> str:
    try:
        with open(path, 'r') as f:
            content = f.read()
        return Ok(content)
    except FileNotFoundError:
        return Err(f"File not found: {path}")
    except PermissionError:
        return Err(f"Permission denied: {path}")

Data Validation

def validate_input(data: str) -> str:
    if not data:
        return Err("Input cannot be empty")
    
    if len(data) > 1000:
        return Err("Input too large (max 1000 characters)")
    
    return Ok(f"Valid input: {data}")

Troubleshooting

Build Errors

  • Ensure Python 3.10+ is installed
  • Verify componentize-py is installed via uv
  • Check that WIT interface matches your Python class

Runtime Errors

  • Validate all imports are available in Wasm environment
  • Check that file paths are correct
  • Review Wassette logs for detailed errors

Import Errors

Some Python libraries may not work in Wasm. Stick to:

  • Standard library modules (json, csv, math, etc.)
  • Pure Python packages
  • Modules explicitly tested with componentize-py

Full Documentation

For complete details, including advanced topics and more examples, see the Python Development Guide.

Working Examples

See this complete working example in the repository:

  • eval-py - Python code execution component

Next Steps

Additional Resources

Building Wasm Components with Rust

This cookbook guide shows you how to build WebAssembly components using Rust that work with Wassette.

Quick Start

Prerequisites

  • Rust toolchain (1.75.0 or later)
  • WASI Preview 2 target

Install Tools

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

# Add WASI target
rustup target add wasm32-wasip2

# Install wit-bindgen (optional, for manual binding generation)
cargo install wit-bindgen-cli --version 0.37.0

Step-by-Step Guide

1. Create Your Project

cargo new --lib my-component
cd my-component

2. Configure Cargo.toml

[package]
name = "my-component"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = { version = "0.37.0", default-features = false }

[profile.release]
opt-level = "s"
lto = true
strip = true

3. Define Your Interface (WIT)

Create wit/world.wit:

package local:my-component;

world calculator {
    export add: func(a: s32, b: s32) -> s32;
    export divide: func(a: f64, b: f64) -> result<f64, string>;
}

4. Generate Bindings

wit-bindgen rust wit/ --out-dir src/ --runtime-path wit_bindgen_rt --async none

5. Implement Your Component

Create/update src/lib.rs:

#![allow(unused)]
fn main() {
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

mod bindings;

use bindings::exports::local::my_component::calculator::Guest;

struct Component;

impl Guest for Component {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    fn divide(a: f64, b: f64) -> Result<f64, String> {
        if b == 0.0 {
            Err("Division by zero".to_string())
        } else {
            Ok(a / b)
        }
    }
}

bindings::export!(Component with_types_in bindings);
}

6. Build Your Component

# Debug build
cargo build --target wasm32-wasip2

# Release build (recommended)
cargo build --target wasm32-wasip2 --release

# Output: target/wasm32-wasip2/release/my_component.wasm

7. Test Your Component

wassette serve --sse --plugin-dir target/wasm32-wasip2/release/

Complete Examples

Simple Calculator

wit/world.wit:

package local:calculator;

world calculator {
    export add: func(a: s32, b: s32) -> s32;
    export subtract: func(a: s32, b: s32) -> s32;
    export multiply: func(a: s32, b: s32) -> s32;
    export divide: func(a: s32, b: s32) -> result<s32, string>;
}

src/lib.rs:

#![allow(unused)]
fn main() {
mod bindings;

use bindings::exports::local::calculator::calculator::Guest;

struct Calculator;

impl Guest for Calculator {
    fn add(a: i32, b: i32) -> i32 {
        a.saturating_add(b)
    }
    
    fn subtract(a: i32, b: i32) -> i32 {
        a.saturating_sub(b)
    }
    
    fn multiply(a: i32, b: i32) -> i32 {
        a.saturating_mul(b)
    }
    
    fn divide(a: i32, b: i32) -> Result<i32, String> {
        if b == 0 {
            Err("Division by zero".to_string())
        } else {
            Ok(a / b)
        }
    }
}

bindings::export!(Calculator with_types_in bindings);
}

HTTP Client

wit/world.wit:

package local:http-client;

world fetch {
    import wasi:http/outgoing-handler@0.2.0;
    
    export fetch-url: func(url: string) -> result<string, string>;
}

src/lib.rs:

#![allow(unused)]
fn main() {
mod bindings;

use bindings::exports::local::http_client::fetch::Guest;
use bindings::wasi::http::outgoing_handler;
use bindings::wasi::http::types::{Method, Scheme};

struct Fetch;

impl Guest for Fetch {
    fn fetch_url(url: String) -> Result<String, String> {
        // Parse URL and create request
        let request = outgoing_handler::OutgoingRequest::new(
            Method::Get,
            Some(&url),
            Scheme::Https,
            None,
        );
        
        // Send request
        match outgoing_handler::handle(request, None) {
            Ok(response) => {
                // Read response body
                Ok("Response received".to_string())
            }
            Err(e) => Err(format!("HTTP error: {:?}", e)),
        }
    }
}

bindings::export!(Fetch with_types_in bindings);
}

File System Operations

wit/world.wit:

package local:filesystem;

world file-ops {
    import wasi:filesystem/types@0.2.0;
    
    export read-file: func(path: string) -> result<string, string>;
    export write-file: func(path: string, content: string) -> result<_, string>;
}

src/lib.rs:

#![allow(unused)]
fn main() {
mod bindings;

use bindings::exports::local::filesystem::file_ops::Guest;
use std::fs;

struct FileOps;

impl Guest for FileOps {
    fn read_file(path: String) -> Result<String, String> {
        fs::read_to_string(&path)
            .map_err(|e| format!("Failed to read {}: {}", path, e))
    }
    
    fn write_file(path: String, content: String) -> Result<(), String> {
        fs::write(&path, content)
            .map_err(|e| format!("Failed to write {}: {}", path, e))
    }
}

bindings::export!(FileOps with_types_in bindings);
}

Error Handling

Rust components use Result<T, E> for error handling:

#![allow(unused)]
fn main() {
// Success
Ok(value)

// Error
Err("Error message".to_string())

// Using the ? operator
fn process_data(input: String) -> Result<String, String> {
    let parsed = parse_input(&input)?;
    let result = transform(parsed)?;
    Ok(result)
}
}

Build Automation with Justfile

Create Justfile for easy building:

install-wasi-target:
    rustup target add wasm32-wasip2

install-bindgen:
    cargo install wit-bindgen-cli --version 0.37.0

generate-bindings: install-bindgen
    wit-bindgen rust wit/ --out-dir src/ --runtime-path wit_bindgen_rt --async none
    @COMPONENT_NAME=$(grep '^name = ' Cargo.toml | sed 's/name = "\(.*\)"/\1/' | tr '-' '_'); \
     if [ -f "src/$${COMPONENT_NAME}.rs" ]; then mv "src/$${COMPONENT_NAME}.rs" src/bindings.rs; fi

build mode="debug": install-wasi-target generate-bindings
    cargo build --target wasm32-wasip2 {{ if mode == "release" { "--release" } else { "" } }}

clean:
    cargo clean
    rm -f src/bindings.rs

test:
    cargo test

all: build

Usage:

just build           # Debug build
just build release   # Release build
just clean          # Clean build artifacts

Best Practices

  1. Use strong typing - Leverage Rust’s type system for safety
  2. Handle errors properly - Always use Result<T, E> for fallible operations
  3. Optimize for size - Use opt-level = "s" and enable LTO in release builds
  4. Avoid unwrap/panic - Return errors instead of panicking
  5. Use saturating operations - Prevent integer overflow with saturating_add, etc.

Common Patterns

String Processing

#![allow(unused)]
fn main() {
fn process_text(input: String) -> Result<String, String> {
    if input.is_empty() {
        return Err("Input cannot be empty".to_string());
    }
    
    let processed = input.to_uppercase();
    Ok(processed)
}
}

JSON Handling (with serde)

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Data {
    name: String,
    value: i32,
}

fn parse_json(json: String) -> Result<String, String> {
    let data: Data = serde_json::from_str(&json)
        .map_err(|e| format!("JSON parse error: {}", e))?;
    
    // Process data
    let result = Data {
        name: data.name.to_uppercase(),
        value: data.value * 2,
    };
    
    serde_json::to_string(&result)
        .map_err(|e| format!("JSON serialize error: {}", e))
}
}

Stateful Components

#![allow(unused)]
fn main() {
use std::sync::Mutex;

static STATE: Mutex<Vec<String>> = Mutex::new(Vec::new());

fn add_item(item: String) -> Result<(), String> {
    let mut state = STATE.lock()
        .map_err(|e| format!("Lock error: {}", e))?;
    state.push(item);
    Ok(())
}

fn get_items() -> Result<Vec<String>, String> {
    let state = STATE.lock()
        .map_err(|e| format!("Lock error: {}", e))?;
    Ok(state.clone())
}
}

Troubleshooting

Build Errors

  • Ensure wasm32-wasip2 target is installed
  • Check that WIT bindings are up to date
  • Verify wit-bindgen version matches dependencies

Linker Errors

  • Make sure crate-type = ["cdylib"] is set in Cargo.toml
  • Check that all imports are properly declared in WIT

Runtime Errors

  • Review WASI permissions in policy configuration
  • Check for panics or unwraps in your code
  • Validate input/output types match WIT interface

Full Documentation

For complete details, including advanced topics and more examples, see the Rust Development Guide.

Working Examples

See these complete working examples in the repository:

Next Steps

Additional Resources

Building Wasm Components with Go

This cookbook guide shows you how to build WebAssembly components using Go and TinyGo that work with Wassette.

Quick Start

Prerequisites

  • Go (version 1.19 through 1.23)
  • TinyGo (version 0.32 or later)

Install Tools

# Install Go from https://golang.org/dl/

# Install TinyGo from https://tinygo.org/getting-started/install/
# On macOS with Homebrew:
brew tap tinygo-org/tools
brew install tinygo

# On Linux:
wget https://github.com/tinygo-org/tinygo/releases/download/v0.32.0/tinygo_0.32.0_amd64.deb
sudo dpkg -i tinygo_0.32.0_amd64.deb

Step-by-Step Guide

1. Create Your Project

mkdir my-component
cd my-component
go mod init example.com/my-component

2. Define Your Interface (WIT)

Create wit/world.wit:

package local:my-component;

world my-component {
    export greet: func(name: string) -> string;
    export calculate: func(a: s32, b: s32) -> s32;
}

3. Generate Go Bindings

go run go.bytecodealliance.org/cmd/wit-bindgen-go@v0.6.2 generate ./wit --out gen

This creates Go bindings in the gen/ directory.

4. Implement Your Component

Create main.go:

package main

import (
    "fmt"
    
    "example.com/my-component/gen"
)

// Component implementation
type Component struct{}

func (c Component) Greet(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

func (c Component) Calculate(a, b int32) int32 {
    return a + b
}

func init() {
    // Export the component
    gen.SetExports(Component{})
}

func main() {}

5. Build Your Component

tinygo build -o component.wasm -target wasip2 --wit-package ./wit --wit-world my-component main.go

6. Test Your Component

wassette serve --sse --plugin-dir .

Complete Examples

Module Information Service

wit/world.wit:

package local:gomodule-server;

world gomodule-server {
    export get-module-info: func(module-path: string) -> result<string, string>;
}

main.go:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
    
    "example.com/gomodule-server/gen"
)

type ModuleInfo struct {
    Path    string `json:"path"`
    Version string `json:"version"`
    Time    string `json:"time"`
}

type Component struct{}

func (c Component) GetModuleInfo(modulePath string) (string, error) {
    // Fetch module information from pkg.go.dev
    url := fmt.Sprintf("https://proxy.golang.org/%s/@latest", modulePath)
    
    resp, err := http.Get(url)
    if err != nil {
        return "", fmt.Errorf("failed to fetch: %v", err)
    }
    defer resp.Body.Close()
    
    var info ModuleInfo
    if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
        return "", fmt.Errorf("failed to parse: %v", err)
    }
    
    result, err := json.Marshal(info)
    if err != nil {
        return "", fmt.Errorf("failed to marshal: %v", err)
    }
    
    return string(result), nil
}

func init() {
    gen.SetExports(Component{})
}

func main() {}

Simple Calculator

wit/world.wit:

package local:calculator;

world calculator {
    export add: func(a: s32, b: s32) -> s32;
    export subtract: func(a: s32, b: s32) -> s32;
    export multiply: func(a: s32, b: s32) -> s32;
    export divide: func(a: s32, b: s32) -> result<s32, string>;
}

main.go:

package main

import (
    "fmt"
    
    "example.com/calculator/gen"
)

type Calculator struct{}

func (c Calculator) Add(a, b int32) int32 {
    return a + b
}

func (c Calculator) Subtract(a, b int32) int32 {
    return a - b
}

func (c Calculator) Multiply(a, b int32) int32 {
    return a * b
}

func (c Calculator) Divide(a, b int32) (int32, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

func init() {
    gen.SetExports(Calculator{})
}

func main() {}

Text Processing

wit/world.wit:

package local:text-processor;

world processor {
    export process-text: func(input: string, operation: string) -> result<string, string>;
}

main.go:

package main

import (
    "fmt"
    "strings"
    
    "example.com/text-processor/gen"
)

type Processor struct{}

func (p Processor) ProcessText(input, operation string) (string, error) {
    switch operation {
    case "uppercase":
        return strings.ToUpper(input), nil
    case "lowercase":
        return strings.ToLower(input), nil
    case "reverse":
        runes := []rune(input)
        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
            runes[i], runes[j] = runes[j], runes[i]
        }
        return string(runes), nil
    default:
        return "", fmt.Errorf("unknown operation: %s", operation)
    }
}

func init() {
    gen.SetExports(Processor{})
}

func main() {}

Error Handling

Go components use the standard Go error type for WIT’s result:

// Success
return result, nil

// Error
return "", fmt.Errorf("error message: %v", err)

// Or with a zero value for the result
return 0, fmt.Errorf("calculation failed")

Working with WIT Types

Type Mappings

// WIT type -> Go type
// s32, s64 -> int32, int64
// u32, u64 -> uint32, uint64
// f32, f64 -> float32, float64
// string -> string
// bool -> bool
// list<T> -> []T
// option<T> -> *T
// result<T, E> -> (T, error)
// record -> struct

Complex Types

// WIT record
// record person {
//     name: string,
//     age: u32,
// }

type Person struct {
    Name string
    Age  uint32
}

func processPerson(p Person) string {
    return fmt.Sprintf("%s is %d years old", p.Name, p.Age)
}

Best Practices

  1. Use proper error handling - Always check and return errors appropriately
  2. Keep components small - TinyGo produces smaller binaries with focused code
  3. Avoid unsupported features - Some Go standard library features may not work with TinyGo
  4. Test thoroughly - Validate your component works with Wassette before deployment
  5. Document interfaces - Use WIT comments to document your API

Common Patterns

JSON Processing

import (
    "encoding/json"
    "fmt"
)

type Data struct {
    Name  string `json:"name"`
    Value int    `json:"value"`
}

func processJSON(jsonStr string) (string, error) {
    var data Data
    if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
        return "", fmt.Errorf("invalid JSON: %v", err)
    }
    
    // Process data
    data.Value *= 2
    
    result, err := json.Marshal(data)
    if err != nil {
        return "", fmt.Errorf("marshal error: %v", err)
    }
    
    return string(result), nil
}

HTTP Client

import (
    "fmt"
    "io"
    "net/http"
)

func fetchURL(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", fmt.Errorf("request failed: %v", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("bad status: %s", resp.Status)
    }
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", fmt.Errorf("read failed: %v", err)
    }
    
    return string(body), nil
}

String Validation

import (
    "fmt"
    "strings"
)

func validateInput(input string) (string, error) {
    if strings.TrimSpace(input) == "" {
        return "", fmt.Errorf("input cannot be empty")
    }
    
    if len(input) > 1000 {
        return "", fmt.Errorf("input too long (max 1000 chars)")
    }
    
    return strings.TrimSpace(input), nil
}

Build Configuration

Using Justfile

Create Justfile for build automation:

# Generate Go bindings from WIT files
generate:
    go run go.bytecodealliance.org/cmd/wit-bindgen-go@v0.6.2 generate ./wit --out gen

# Build the component
build: generate
    tinygo build -o component.wasm -target wasip2 --wit-package ./wit --wit-world my-component main.go

# Build with optimizations
build-release: generate
    tinygo build -o component.wasm -target wasip2 --wit-package ./wit --wit-world my-component -opt=2 main.go

# Clean build artifacts
clean:
    rm -rf gen/
    rm -f component.wasm

# Test the component
test: build
    wassette serve --sse --plugin-dir .

Usage:

just build          # Build component
just build-release  # Build with optimizations
just clean         # Clean artifacts

Troubleshooting

Build Errors

TinyGo version issues:

  • Ensure TinyGo supports your Go version (currently 1.19-1.23)
  • Update TinyGo to the latest version

WIT binding errors:

  • Regenerate bindings after WIT changes
  • Check that wit-bindgen-go version is compatible

Import errors:

  • Some Go packages may not work with TinyGo
  • Use TinyGo-compatible alternatives or implement manually

Runtime Errors

Component not loading:

  • Verify WIT interface matches implementation
  • Check that all exported functions are implemented
  • Review Wassette logs for details

Performance issues:

  • Use -opt=2 flag for optimized builds
  • Profile your code to find bottlenecks
  • Consider using Rust for performance-critical components

TinyGo Limitations

Some Go features are not available in TinyGo:

  • Some reflection features
  • Full goroutine support (limited)
  • Some standard library packages
  • CGO

See TinyGo language support for details.

Full Documentation

For complete details, including advanced topics and more examples, see the Go Development Guide.

Working Examples

See this complete working example in the repository:

Next Steps

Additional Resources

Wassette CLI Reference

The Wassette command-line interface provides comprehensive tools for managing WebAssembly components, policies, and permissions both locally and through the MCP server. This document covers all CLI functionality and usage patterns.

Overview

Wassette offers two primary modes of operation:

  1. Server Mode: Run as an MCP server that responds to client requests
  2. CLI Mode: Direct command-line management of components and permissions

The CLI mode allows you to perform administrative tasks without requiring a running MCP server, making it ideal for automation, scripting, and local development workflows.

Installation

For installation instructions, see the main README. Once installed, the wassette command will be available in your PATH.

Quick Start

# Check available commands
wassette --help

# List currently loaded components
wassette component list

# Load a component from an OCI registry
wassette component load oci://ghcr.io/yoshuawuyts/time:latest

# Load a component from a local file
wassette component load file:///path/to/component.wasm

# Start the MCP server (traditional mode)
wassette serve --stdio

Command Structure

Wassette uses a hierarchical command structure organized around functional areas:

wassette
├── serve          # Start MCP server
├── component      # Component lifecycle management
│   ├── load       # Load components
│   ├── unload     # Remove components
│   └── list       # Show loaded components
├── policy         # Policy information
│   └── get        # Retrieve component policies
└── permission     # Permission management
    ├── grant      # Add permissions
    ├── revoke     # Remove permissions
    └── reset      # Clear all permissions

Server Commands

wassette serve

Start the Wassette MCP server to handle client requests.

Stdio Transport (recommended for MCP clients):

# Start server with stdio transport
wassette serve --stdio

# Use with specific configuration directory
wassette serve --stdio --plugin-dir /custom/components

HTTP Transport (for development and debugging):

# Start server with HTTP transport
wassette serve --http

# Use Server-Sent Events (SSE) transport
wassette serve --sse

Options:

  • --stdio: Use stdio transport (recommended for MCP clients)
  • --http: Use HTTP transport on 127.0.0.1:9001
  • --sse: Use Server-Sent Events transport
  • --plugin-dir <PATH>: Set component storage directory (default: $XDG_DATA_HOME/wassette/components)

Component Management

wassette component load

Load a WebAssembly component from various sources.

Load from OCI registry:

# Load a component from GitHub Container Registry
wassette component load oci://ghcr.io/yoshuawuyts/time:latest

# Load with custom plugin directory
wassette component load oci://ghcr.io/microsoft/gomodule:latest --plugin-dir /custom/components

Load from local file:

# Load a local component file
wassette component load file:///path/to/component.wasm

# Load with relative path
wassette component load file://./my-component.wasm

Options:

  • --plugin-dir <PATH>: Component storage directory

wassette component unload

Remove a loaded component by its ID.

# Unload a component
wassette component unload my-component-id

# Unload with custom plugin directory
wassette component unload my-component-id --plugin-dir /custom/components

Options:

  • --plugin-dir <PATH>: Component storage directory

wassette component list

Display all currently loaded components.

Basic JSON output:

wassette component list
# Output: {"components":[...],"total":1}

Formatted output options:

# Pretty-printed JSON
wassette component list --output-format json

# YAML format
wassette component list --output-format yaml

# Table format (human-readable)
wassette component list --output-format table

Example outputs:

JSON format:

{
  "components": [
    {
      "id": "time-component",
      "schema": {
        "tools": [
          {
            "name": "get-current-time",
            "description": "Get the current time",
            "inputSchema": {
              "type": "object",
              "properties": {}
            }
          }
        ]
      },
      "tools_count": 1
    }
  ],
  "total": 1
}

Table format:

ID             | Tools | Description
---------------|-------|----------------------------------
time-component | 1     | Provides time-related functions

Options:

  • --output-format <FORMAT>: Output format (json, yaml, table) [default: json]
  • --plugin-dir <PATH>: Component storage directory

Policy Management

wassette policy get

Retrieve policy information for a specific component.

# Get policy for a component
wassette policy get my-component-id

# Get policy with pretty formatting
wassette policy get my-component-id --output-format json

# Get in YAML format
wassette policy get my-component-id --output-format yaml

Example output:

{
  "component_id": "my-component",
  "permissions": {
    "storage": [
      {
        "uri": "fs://workspace/**",
        "access": ["read", "write"]
      }
    ],
    "network": [
      {
        "host": "api.openai.com"
      }
    ]
  }
}

Options:

  • --output-format <FORMAT>: Output format (json, yaml, table) [default: json]
  • --plugin-dir <PATH>: Component storage directory

Permission Management

wassette permission grant

Grant specific permissions to a component.

Storage permissions:

# Grant read access to a directory
wassette permission grant storage my-component fs://workspace/ --access read

# Grant read and write access
wassette permission grant storage my-component fs://workspace/ --access read,write

# Grant access to a specific file
wassette permission grant storage my-component fs://config/app.yaml --access read

Network permissions:

# Grant access to a specific host
wassette permission grant network my-component api.openai.com

# Grant access to a localhost service
wassette permission grant network my-component localhost:8080

Environment variable permissions:

# Grant access to an environment variable
wassette permission grant environment-variable my-component API_KEY

# Grant access to multiple variables
wassette permission grant environment-variable my-component HOME
wassette permission grant environment-variable my-component PATH

Memory permissions:

# Grant memory limit to a component (using Kubernetes format)
wassette permission grant memory my-component 512Mi

# Grant larger memory limit
wassette permission grant memory my-component 1Gi

# Grant memory limit with different units
wassette permission grant memory my-component 2048Ki

Options:

  • --access <ACCESS>: For storage permissions, comma-separated list of access types (read, write)
  • --plugin-dir <PATH>: Component storage directory

wassette permission revoke

Remove specific permissions from a component.

Storage permissions:

# Revoke storage access
wassette permission revoke storage my-component fs://workspace/

# Revoke with custom plugin directory
wassette permission revoke storage my-component fs://config/ --plugin-dir /custom/components

Network permissions:

# Revoke network access
wassette permission revoke network my-component api.openai.com

Environment variable permissions:

# Revoke environment variable access
wassette permission revoke environment-variable my-component API_KEY

Options:

  • --plugin-dir <PATH>: Component storage directory

wassette permission reset

Remove all permissions for a component, resetting it to default state.

# Reset all permissions for a component
wassette permission reset my-component

# Reset with custom plugin directory
wassette permission reset my-component --plugin-dir /custom/components

Options:

  • --plugin-dir <PATH>: Component storage directory

Common Workflows

Local Development

# 1. Build and load a local component
wassette component load file://./target/wasm32-wasi/debug/my-tool.wasm

# 2. Check it loaded correctly
wassette component list --output-format table

# 3. Grant necessary permissions
wassette permission grant storage my-tool fs://$(pwd)/workspace --access read,write
wassette permission grant network my-tool api.example.com
wassette permission grant memory my-tool 512Mi

# 4. Verify permissions
wassette policy get my-tool --output-format yaml

# 5. Test via MCP server
wassette serve --stdio

Component Distribution

# 1. Load component from OCI registry
wassette component load oci://ghcr.io/myorg/my-tool:v1.0.0

# 2. Configure permissions based on component needs
wassette permission grant storage my-tool fs://workspace/** --access read,write
wassette permission grant network my-tool api.myservice.com
wassette permission grant memory my-tool 1Gi

# 3. Start server for clients
wassette serve --sse

Permission Auditing

# List all components and their tool counts
wassette component list --output-format table

# Check permissions for each component
for component in $(wassette component list | jq -r '.components[].id'); do
  echo "=== $component ==="
  wassette policy get $component --output-format yaml
done

Cleanup Operations

# Reset permissions for a component
wassette permission reset problematic-component

# Remove a component entirely
wassette component unload problematic-component

# List remaining components
wassette component list --output-format table

Configuration

Wassette can be configured using configuration files, environment variables, and command-line options. The configuration sources are merged with the following order of precedence:

  1. Command-line options (highest priority)
  2. Environment variables prefixed with WASSETTE_
  3. Configuration file (lowest priority)

Configuration File

By default, Wassette looks for a configuration file at:

  • Linux/macOS: $XDG_CONFIG_HOME/wassette/config.toml (typically ~/.config/wassette/config.toml)
  • Windows: %APPDATA%\wassette\config.toml

You can override the default configuration file location using the WASSETTE_CONFIG_FILE environment variable:

export WASSETTE_CONFIG_FILE=/custom/path/to/config.toml
wassette component list

Example configuration file (config.toml):

# Directory where components are stored
plugin_dir = "/opt/wassette/components"

Environment Variables

  • WASSETTE_CONFIG_FILE: Override the default configuration file location
  • WASSETTE_PLUGIN_DIR: Override the default component storage location
  • XDG_CONFIG_HOME: Base directory for configuration files (Linux/macOS)
  • XDG_DATA_HOME: Base directory for data storage (Linux/macOS)

Component Storage

By default, Wassette stores components in $XDG_DATA_HOME/wassette/components (typically ~/.local/share/wassette/components on Linux/macOS). You can override this with the --plugin-dir option:

# Use custom storage directory
export WASSETTE_PLUGIN_DIR=/opt/wassette/components
wassette component load oci://example.com/tool:latest --plugin-dir $WASSETTE_PLUGIN_DIR

Integration with MCP Clients

The CLI commands complement the MCP server functionality. You can:

  1. Use CLI commands to pre-configure components and permissions
  2. Start the MCP server with wassette serve
  3. Connect MCP clients to the running server
  4. Use CLI commands for administrative tasks while the server runs

Example VS Code configuration:

{
  "name": "wassette",
  "command": "wassette",
  "args": ["serve", "--stdio"]
}

Error Handling

The CLI provides clear error messages for common issues:

# Component not found
$ wassette component unload nonexistent
Error: Component 'nonexistent' not found

# Invalid path
$ wassette component load invalid://path
Error: Unsupported URI scheme 'invalid'. Use 'file://' or 'oci://'

# Permission denied
$ wassette permission grant storage my-component /restricted --access write
Error: Permission denied: cannot grant write access to /restricted

Output Formats

All commands that return structured data support multiple output formats:

  • JSON (default): Machine-readable, suitable for scripting
  • YAML: Human-readable structured format
  • Table: Formatted for terminal display

Use the --output-format or -o flag to specify the desired format:

wassette component list -o table
wassette policy get my-component -o yaml

See Also

sequenceDiagram
    participant Client as MCP Client
    participant Server as Wassette MCP Server
    participant LM as LifecycleManager
    participant Engine as Wasmtime Engine
    participant Registry as Component Registry
    participant Policy as Policy Engine
    
    Client->>Server: load-component(path)
    Server->>LM: load_component(uri)
    
    alt OCI Registry
        LM->>LM: Download from OCI
    else Local File
        LM->>LM: Load from filesystem
    else HTTP URL
        LM->>LM: Download from URL
    end
    
    LM->>Engine: Create Component
    Engine->>Engine: Compile WebAssembly
    Engine->>Engine: Extract WIT Interface
    
    LM->>Registry: Register Component
    Registry->>Registry: Generate JSON Schema
    Registry->>Registry: Map Tools to Component
    
    LM->>Policy: Apply Default Policy
    Policy->>Policy: Create WASI State Template
    
    LM-->>Server: Component ID + LoadResult
    Server-->>Client: Success with ID
    
    Note over Client,Policy: Component is now loaded and ready
    
    Client->>Server: call_tool(tool_name, args)
    Server->>LM: execute_component_call(id, func, params)
    
    LM->>Policy: Get WASI State for Component
    Policy->>Policy: Apply Security Policy
    Policy->>Engine: Create Store with WASI Context
    
    LM->>Engine: Instantiate Component
    Engine->>Engine: Call Function with Args
    Engine->>Engine: Execute in Sandbox
    
    Engine-->>LM: Results
    LM-->>Server: JSON Response
    Server-->>Client: Tool Result

Permission System Design

  • Author: @Mossaka
  • Date: 2025-07-11

Overview

The permission system provides fine-grained, per-component capability controls in the Wassette MCP server. This document describes the architecture,implementation, and future development plans.

Architecture Overview

Core Components

graph TB
    Client[MCP Client] --> Server[Wassette MCP Server]
    Server --> LM[LifecycleManager]
    LM --> PR[PolicyRegistry]
    LM --> CR[ComponentRegistry]
    LM --> Engine[Wasmtime Engine]
    
    LM --> Resources[Resources]
    PR --> Permissions[Permissions]

    subgraph "Resources"
        PolicyFile[Component Policy Files]
        ComponentFile[Component.wasm]
    end
    
    subgraph "Permissions"
        Network[Network Permissions]
        Storage[Storage Permissions]
        Environment[Environment Permissions]
    end

Key Design Principles

  • Per-Component Policies: Each component has its own permission policy, co-located with the component binary under the plugin_dir directory
  • Principle of Least Privilege: Components only get permissions they need
  • Dynamic Control: Permissions can be granted at runtime using the grant-permission tool

Current Implementation Status

1. Per-Component Policy System

Status: ✅ Implemented

Each component can have its own policy file stored as {component_id}.policy.yaml co-located with the component binary.

#![allow(unused)]
fn main() {
// Current Implementation
struct PolicyRegistry {
    component_policies: HashMap<String, Arc<WasiStateTemplate>>,
}

impl LifecycleManager {
    async fn get_wasi_state_for_component(&self, component_id: &str) -> Result<WasiState> {
        // Returns component-specific WASI state or default restrictive policy
    }
}
}

2. Policy Lifecycle Management

Status: ✅ Implemented

Built-in tools for managing component policies:

  • attach-policy: Attach policy from file:// or https:// URI
  • detach-policy: Remove policy from component
  • get-policy: Get policy information for component

3. Granular Permission System

Status: ✅ Implemented

The grant_storage_permission method allows atomic permission grants:

#![allow(unused)]
fn main() {
pub async fn grant_storage_permission(
    &self,
    component_id: &str,
    details: &serde_json::Value,
) -> Result<()>
}

The grant_network_permission method allows atomic permission grants:

#![allow(unused)]
fn main() {
pub async fn grant_network_permission(
    &self,
    component_id: &str,
    details: &serde_json::Value,
) -> Result<()>
}

Supported permission types:

  • Network: {"host": "api.example.com"}
  • Storage: {"uri": "fs:///path", "access": ["read", "write"]}

4. Policy Persistence

Status: ✅ Implemented

  • Policies are stored co-located with components
  • Policy associations are restored on server restart
  • Metadata tracking for policy sources

Built-in Tools

  1. get-policy: Get policy information
  2. grant-storage-permission: Grant storage access
  3. grant-network-permission: Grant network access
  4. grant-environment-variable-permission: Grant environment variable access
  5. revoke-storage-permission: Revoke storage access permissions
  6. revoke-network-permission: Revoke network access permissions
  7. revoke-environment-variable-permission: Revoke environment variable access permissions
  8. reset-permission: Reset all permissions for a component
  9. load-component: Load WebAssembly component
  10. unload-component: Unload component
  11. list-components: List loaded components
  12. search-components: Search available components from registry

Permission Types and Structure

Policy File Format

version: "1.0"
description: "Component-specific security policy"
permissions:
  network:
    allow:
      - host: "api.example.com"
      - host: "cdn.example.com"
  environment:
    allow:
      - key: "API_KEY"
      - key: "CONFIG_URL"
  storage:
    allow:
      - uri: "fs:///tmp/workspace"
        access: ["read", "write"]
      - uri: "fs:///var/cache"
        access: ["read"]

Future Development Roadmap

  • Policy Signing: Verify policy integrity with signatures
  • Policy Checksums: Verify downloaded policy integrity
  • Policy Caching: Optimize policy loading and parsing

Component Schemas and Structured Output

New contributors quickly encounter two pieces of infrastructure when working on Wassette’s component runtime:

  1. The component2json crate, which introspects WebAssembly Components and converts their WebAssembly Interface Types (WIT) into JSON-friendly schemas and values.
  2. The structured output utilities in wassette::schema, which normalize those schemas and align runtime values so Model Context Protocol (MCP) clients always see a predictable { "result": ... } envelope.

This document explains how the two layers interact, why the result wrapper exists, and what to keep in mind when changing either side. It is intended for engineers extending the schema pipeline or adding new tooling around component execution.

High-Level Flow

WIT component  ──► component2json::component_exports_to_tools
                    │
                    ▼
             tool metadata with `outputSchema`
                    │
                    ▼
       wassette::schema::canonicalize_output_schema
                    │
                    ▼
        registry + MCP-facing structured responses

The flow repeats in three places:

  • During component load we call component_exports_to_tools to populate the in-memory registry and cache metadata on disk.
  • When serving metadata to clients we run canonicalize_output_schema to guarantee the result wrapper and tuple-normalization appear even if the cached schema predates the newer format.
  • When returning call responses we run ensure_structured_result so the JSON payload we return in structured_content matches the schema that was advertised.

component2json Responsibilities

component2json handles three jobs:

  1. Schema generationcomponent_exports_to_json_schema and component_exports_to_tools walk the component exports and convert each parameter and result type to JSON Schema.
  2. Value conversionjson_to_vals converts incoming JSON arguments into WIT Vals, while vals_to_json converts WIT results back to JSON.
  3. Result envelope – all non-empty result sets are wrapped in { "result": ... }. For multi-value returns, each position is named val0, val1, etc so the metadata remains stable even when the component author reorders tuple fields.

Quick Reference

FunctionPurpose
component_exports_to_json_schemaReturns { "tools": [ ... ] } for every exported function.
component_exports_to_toolsLower-level API returning ToolMetadata structs.
type_to_json_schemaCore translator from WIT Type to JSON Schema.
vals_to_jsonConverts [Val] results into the canonical JSON wrapper.
json_to_valsConverts JSON arguments into [Val] based on (name, Type) pairs.
create_placeholder_resultsAllocates correctly-typed buffers before invoking a component.

Example: Single Return Value

Consider a component function defined in WIT as:

package example:math

interface calculator {
    use wasi:clocks/monotonic-clock

    /// Adds one to the given integer.
    add-one: func(x: s32) -> s32
}

Running component_exports_to_tools produces (simplified) metadata:

{
  "name": "example_math_calculator_add_one",
  "inputSchema": {
    "type": "object",
    "properties": { "x": { "type": "number" } },
    "required": ["x"]
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "result": { "type": "number" }
    },
    "required": ["result"]
  }
}

When the function executes and returns 42, vals_to_json emits:

{ "result": 42 }

Even though only a single scalar is returned, the wrapper ensures clients always access the payload through the result property.

Example: Multiple Return Values

Suppose the component exposes:

interface time-range {
    /// Returns the (start, end) timestamps in nanoseconds.
    span: func() -> tuple<u64, u64>
}

The generated schema becomes:

{
  "type": "object",
  "properties": {
    "result": {
      "type": "object",
      "properties": {
        "val0": { "type": "number" },
        "val1": { "type": "number" }
      },
      "required": ["val0", "val1"]
    }
  },
  "required": ["result"]
}

And the runtime result for (123, 456) is

{ "result": { "val0": 123, "val1": 456 } }

The positional names (val0, val1, …) keep the schema stable and make it easy for language- agnostic MCP clients to reason about tuple-like output.

Example: Result Types

component2json also understands result<T, E> shapes. Given a WIT signature:

interface fetcher {
    fetch: func(url: string) -> result<string, string>
}

The outputSchema includes the result wrapper and the familiar oneOf structure for ok and err variants:

{
  "type": "object",
  "properties": {
    "result": {
      "type": "object",
      "oneOf": [
        {
          "type": "object",
          "properties": { "ok": { "type": "string" } },
          "required": ["ok"]
        },
        {
          "type": "object",
          "properties": { "err": { "type": "string" } },
          "required": ["err"]
        }
      ]
    }
  },
  "required": ["result"]
}

Value Conversion Helpers

create_placeholder_results and json_to_vals are mostly used inside Wassette, but you may need them when writing integration tests or CLIs:

#![allow(unused)]
fn main() {
let params = vec![
    ("url".to_string(), Type::String),
];
let json_args = serde_json::json!({ "url": "https://example.com" });
let wit_vals = json_to_vals(&json_args, &params)?;

// Later, when the component returns:
let raw_results: Vec<Val> = component_call()?;
let json_result = vals_to_json(&raw_results);
assert!(json_result.get("result").is_some());
}

Canonicalization in Wassette

component2json always emits the result wrapper, but Wassette defensively normalizes schemas from multiple sources:

  • Fresh introspection when a component is loaded
  • Cached metadata stored on disk
  • Third-party tooling that may not yet wrap results

The wassette::schema module provides three key helpers.

canonicalize_output_schema

Ensures the schema we pass to clients has:

  1. A top-level object with a required result property.
  2. Tuple-like arrays converted into { "val0": ..., "val1": ... } objects.
  3. Nested schemas recursively normalized.

This runs whenever we:

  • Load schemas from disk (LifecycleManager::populate_registry_from_metadata).
  • Return schema data to clients (get_component_schema, handle_list_components).

ensure_structured_result

Aligns actual runtime output with the canonical schema. It:

  • Inserts the { "result": ... } wrapper if the component returned a bare value.
  • Rewrites arrays to the valN object shape when necessary.
  • Fills in missing object properties with null to match the schema.

This function is applied in handle_component_call before we set structured_content in the MCP response.

wrap_schema_in_result

A small helper used by the other functions; exposed for completeness. It is helpful when building synthetic schemas (e.g., tests that fake tool metadata) because it mirrors the runtime behavior.

End-to-End Example: Fetch Tool

The integration test in tests/structured_output_integration_test.rs exercises the full pipeline. Below is a trimmed version showing the key checkpoints:

#![allow(unused)]
fn main() {
let fetch_tool = tools.iter()
    .find(|tool| tool["name"] == "fetch")
    .expect("fetch tool present");

let output_schema = fetch_tool["outputSchema"].clone();
let canonical = canonicalize_output_schema(&output_schema);
assert!(canonical["properties"]["result"].is_object());

let response_json = lifecycle_manager
    .execute_component_call(&component_id, "fetch", request_json)
    .await?;

let structured_value = ensure_structured_result(&canonical, serde_json::from_str(&response_json)?);
assert!(structured_value["result"].is_object());
}

If the component returns an error, the wrapper still appears:

{
  "result": {
    "err": "network unavailable"
  }
}

That predictable shape is what makes it possible for MCP clients to handle successes and failures without special-casing each tool.

How Metadata Caching Uses These Pieces

When we save component metadata for fast start-up, we serialize the tool schemas exactly as produced by component2json. Upon restart, populate_registry_from_metadata canonicalizes each schema before re-registering it. This guards against older metadata that may lack the wrapper and ensures all run-time code operates on the same normalized representation.

Key call sites:

  • LifecycleManager::ensure_component_loaded – introspects a live component, stores the raw schema, and updates the registry with canonicalized copies.
  • LifecycleManager::populate_registry_from_metadata – reads cached schemas, canonicalizes, then registers them.
  • LifecycleManager::get_component_schema – canonicalizes again just before returning JSON to CLI or MCP clients.

Tips for Contributors

  • Always return structured values through vals_to_json. If you add a new code path that constructs responses manually, wrap them with ensure_structured_result or reuse vals_to_json to avoid schema drift.
  • Update tests when tweaking schemas. The integration test mentioned above is the best safety net. Add new assertions that validate the result wrapper for your scenario.
  • Prefer component_exports_to_tools over manual schema creation. It already handles nested components, package names, and the result wrapper.
  • Use canonicalization helpers in custom tooling. If you build CLIs or services on top of Wassette metadata, calling canonicalize_output_schema will keep your consumers aligned with the server’s behavior.

Additional Scenarios

Custom Structured Payloads

If a component returns an object directly (for example, a record of file metadata), the schema looks like this:

{
  "result": {
    "type": "object",
    "properties": {
      "path": { "type": "string" },
      "size": { "type": "number" }
    },
    "required": ["path", "size"]
  }
}

At runtime the payload is wrapped without modifying the inner object:

{
  "result": {
    "path": "./Cargo.toml",
    "size": 4096
  }
}

Mixing Tuples and Records

WIT supports complex types such as result<(string, u32), error-record>. The generated schema nests both tuple normalization and structured objects:

{
  "result": {
    "oneOf": [
      {
        "type": "object",
        "properties": {
          "ok": {
            "type": "object",
            "properties": {
              "val0": { "type": "string" },
              "val1": { "type": "number" }
            },
            "required": ["val0", "val1"]
          }
        },
        "required": ["ok"]
      },
      {
        "type": "object",
        "properties": {
          "err": {
            "type": "object",
            "properties": {
              "code": { "type": "number" },
              "message": { "type": "string" }
            },
            "required": ["code", "message"]
          }
        },
        "required": ["err"]
      }
    ]
  }
}

No extra work is required in Wassette—the canonicalizer recognizes both patterns automatically.

Handling Empty Results

A function that returns no values produces outputSchema: null. canonicalize_output_schema converts that into None, and ensure_structured_result skips wrapping the response. Consumers can still rely on the result key for all functions that actually return data.

Where to Look in the Code

  • crates/component2json/src/lib.rs – schema translation, value conversion, result wrapper.
  • crates/wassette/src/schema.rs – canonicalization helpers.
  • crates/mcp-server/src/components.rs – wiring between lifecycle manager and MCP responses.
  • tests/structured_output_integration_test.rs – end-to-end assertions covering the entire stack.

Understanding this pipeline makes it easier to add new type translations, improve client ergonomics, or debug mismatched schemas.