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
- 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.
- Direct binary execution. Running binaries directly using
npx
oruvx
. 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. - 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
Quick Install Script (Recommended)
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 System | Architecture | Support |
---|---|---|
Linux | x86_64 (amd64) | ✅ Full support |
Linux | ARM64 (aarch64) | ✅ Full support |
macOS | Intel (x86_64) | ✅ Full support |
macOS | Apple Silicon (ARM64) | ✅ Full support |
Windows | x86_64 | ✅ Full support |
Windows | ARM64 | ✅ Full support |
Windows Subsystem for Linux | x86_64, ARM64 | ✅ Full support |
Next Steps
Once Wassette is installed, you’ll need to configure it with your AI agent:
-
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
-
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
-
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:
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.
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:
- JavaScript (time-server-js, get-weather-js)
- Python (eval-py)
- Rust (fetch-rs, filesystem-rs)
- Go (gomodule-go)
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?
- Define your interface using WebAssembly Interface Types (WIT)
- Implement the functionality in your preferred supported language
- Compile to a Component using appropriate tooling for your language
- 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 accessgrant-network-permission
: Grant network accessgrant-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 componentsunload-component
: Unload componentslist-components
: List loaded componentsget-policy
: Get policy informationgrant-storage-permission
: Grant storage accessgrant-network-permission
: Grant network accessgrant-environment-variable-permission
: Grant environment variable accessrevoke-storage-permission
: Revoke storage access permissionsrevoke-network-permission
: Revoke network access permissionsrevoke-environment-variable-permission
: Revoke environment variable access permissionsreset-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?
- Check the logs: Run Wassette with
RUST_LOG=debug
for detailed logging - Verify permissions: Ensure your policy file grants necessary permissions
- Test component separately: Validate that your component works outside Wassette
- 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?
- GitHub Issues: Report bugs or request features
- Discord: Join the
#wassette
channel on Microsoft Open Source Discord - Documentation: Browse the docs directory for detailed guides
- Examples: Review working examples for common patterns
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:
- Time server (JavaScript)
- Weather API (JavaScript)
- File system operations (Rust)
- HTTP client (Rust)
- Code execution (Python)
- Go module info (Go)
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.
Using MCP Built-in Tools (Recommended)
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 accessgrant-network-permission
: Grant network accessgrant-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 descriptionpermissions
: Permission declarations organized by typestorage.allow
: List of file system URIs and access typesnetwork.allow
: List of allowed hostsenvironment.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:
- Load component in test environment
- Grant minimal permissions
- Test functionality
- Add permissions incrementally as needed
- Document final permission set
Troubleshooting
Component Cannot Access Files
Symptom: Component fails when trying to read or write files.
Solution:
- Check current permissions:
wassette policy get <component-id>
- Verify the file path matches the policy URI
- Ensure access level includes required operations (read/write)
- Grant missing permissions:
wassette permission grant storage <component-id> fs://path --access read,write
Network Requests Failing
Symptom: Component cannot make network requests.
Solution:
- Check current permissions:
wassette policy get <component-id>
- Verify the host is in the allow list
- Check for typos in host names
- Grant missing permissions:
wassette permission grant network <component-id> api.example.com
Environment Variables Not Available
Symptom: Component cannot read environment variables.
Solution:
- Check current permissions:
wassette policy get <component-id>
- Verify the variable key is in the allow list
- Ensure the environment variable is set in your shell
- 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:
- The WebAssembly sandbox blocks the attempt - No unauthorized access occurs
- The operation fails - The component receives an error
- No security exceptions are raised - This is expected behavior
- Logs record the attempt - Check logs with
RUST_LOG=debug
This deny-by-default behavior ensures components cannot exceed their granted capabilities.
Next Steps
- CLI Reference: Complete CLI command documentation
- FAQ: Common questions about security and permissions
- Permission System Design: Technical architecture details
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:
- Start with the language you know best - Each guide is self-contained and provides all the necessary context
- Review the Architecture documentation - Understand how Wassette works with Wasm components
- Check out the Examples - See working implementations in action
- 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 capabilitiesrandom
- Random number generationstdio
- Standard input/outputfilesystem
- File system accessclocks
- 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
- Use clear interface definitions - Make your WIT interfaces descriptive and well-documented
- Handle errors properly - Always use
result<T, string>
for operations that can fail - Keep components focused - Each component should do one thing well
- Test thoroughly - Validate your component works before deploying
- 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:
- time-server-js - Simple time server
- get-weather-js - Weather API client
- get-open-meteo-weather-js - Open-Meteo weather service
Next Steps
- Review the complete JavaScript guide
- Check out working examples
- Learn about Wassette’s architecture
- Read the FAQ
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
- Use type hints - Python type hints help catch errors early
- Handle errors properly - Always return
Ok
orErr
for result types - Document your code - Use docstrings to explain functionality
- Test thoroughly - Validate edge cases and error conditions
- Keep it simple - Avoid complex dependencies that might not work in Wasm
- Avoid
eval()
for untrusted input - Useast.literal_eval()
or proper parsers instead ofeval()
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
- Review the complete Python guide
- Check out working examples
- Learn about componentize-py
- Read the FAQ
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
- Use strong typing - Leverage Rust’s type system for safety
- Handle errors properly - Always use
Result<T, E>
for fallible operations - Optimize for size - Use
opt-level = "s"
and enable LTO in release builds - Avoid unwrap/panic - Return errors instead of panicking
- 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:
- filesystem-rs - File system operations
- fetch-rs - HTTP client
Next Steps
- Review the complete Rust guide
- Check out working examples
- Learn about wit-bindgen
- Read the FAQ
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
- Use proper error handling - Always check and return errors appropriately
- Keep components small - TinyGo produces smaller binaries with focused code
- Avoid unsupported features - Some Go standard library features may not work with TinyGo
- Test thoroughly - Validate your component works with Wassette before deployment
- 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:
- gomodule-go - Go module information service
Next Steps
- Review the complete Go guide
- Check out working examples
- Learn about TinyGo
- Read the FAQ
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:
- Server Mode: Run as an MCP server that responds to client requests
- 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:
- Command-line options (highest priority)
- Environment variables prefixed with
WASSETTE_
- 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 locationWASSETTE_PLUGIN_DIR
: Override the default component storage locationXDG_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:
- Use CLI commands to pre-configure components and permissions
- Start the MCP server with
wassette serve
- Connect MCP clients to the running server
- 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
- Main README - Installation and basic usage
- MCP Client Setup - Configuring MCP clients
- Architecture Overview - Understanding Wassette’s design
- Examples - Sample WebAssembly components
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:// URIdetach-policy
: Remove policy from componentget-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
get-policy
: Get policy informationgrant-storage-permission
: Grant storage accessgrant-network-permission
: Grant network accessgrant-environment-variable-permission
: Grant environment variable accessrevoke-storage-permission
: Revoke storage access permissionsrevoke-network-permission
: Revoke network access permissionsrevoke-environment-variable-permission
: Revoke environment variable access permissionsreset-permission
: Reset all permissions for a componentload-component
: Load WebAssembly componentunload-component
: Unload componentlist-components
: List loaded componentssearch-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:
- The
component2json
crate, which introspects WebAssembly Components and converts their WebAssembly Interface Types (WIT) into JSON-friendly schemas and values. - 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 instructured_content
matches the schema that was advertised.
component2json Responsibilities
component2json
handles three jobs:
- Schema generation –
component_exports_to_json_schema
andcomponent_exports_to_tools
walk the component exports and convert each parameter and result type to JSON Schema. - Value conversion –
json_to_vals
converts incoming JSON arguments into WITVal
s, whilevals_to_json
converts WIT results back to JSON. - Result envelope – all non-empty result sets are wrapped in
{ "result": ... }
. For multi-value returns, each position is namedval0
,val1
, etc so the metadata remains stable even when the component author reorders tuple fields.
Quick Reference
Function | Purpose |
---|---|
component_exports_to_json_schema | Returns { "tools": [ ... ] } for every exported function. |
component_exports_to_tools | Lower-level API returning ToolMetadata structs. |
type_to_json_schema | Core translator from WIT Type to JSON Schema. |
vals_to_json | Converts [Val] results into the canonical JSON wrapper. |
json_to_vals | Converts JSON arguments into [Val] based on (name, Type) pairs. |
create_placeholder_results | Allocates 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, ¶ms)?; // 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:
- A top-level object with a required
result
property. - Tuple-like arrays converted into
{ "val0": ..., "val1": ... }
objects. - 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 withensure_structured_result
or reusevals_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.