Modulewise Toolbelt: Part 3
Published: 8/12/2025
By: Mark Fisher
In the first two parts of this series, we covered the zero-trust execution model and the least-privilege capability model, two key reasons the Modulewise Toolbelt builds upon a Wasm foundation. Those posts were focused on the underlying mechanics and so their examples featured generic Tools. In this third and final post of the series, we will unleash the power of the Wasm Component Model to build a domain-driven demo that exemplifies the Modulewise theme of composable integration, especially highlighting the point that in the enterprise, “intelligent systems” incorporate existing APIs and data.
The demo is based on a Travel Agent having access to Tools for managing Flights and Hotels. The Agent and the “existing” API were built with Spring, so it seemed appropriate for the generated data to have Spring and AI related themes. Here’s an example of these Tools in action:
What does it mean for a Tool to be a domain-driven composition?
To answer that, we will walk through each layer. We just saw an Agent interacting with these Tools, so we’ll pick up with the Tool layer and work our way down to the API.

The Tool Layer
Tools define the boundary between decisions and actions. Agents consult Models to make decisions, and they use Tools to perform actions. Models rely on stochastic processes, so their decision-making is inherently unpredictable. As a counterbalance, Tools should ensure predictable actions. If the inputs and/or outputs to Tools are unstructured text, this decision/action boundary is blurred. To enable a dialog between Agents and Tools in the language of the domain, structured types matter in both directions.
Fortunately, the latest version of the MCP spec (2025-06-18) includes output-schema. This is what an Agent sees when calling tools/list
on a Toolbelt MCP Server hosting the flight search Tool (with nested detail removed for conciseness):
{
"name": "flights_search-flights",
"description": "Call search-flights function from flights component",
"inputSchema": {
"additionalProperties": false,
"properties": {
"arrival": { ... },
"departure": { ... },
"destination": { ... },
"flex": { ... },
"origin": { ... }
},
"required": [ "origin", "destination", "departure" ],
"type": "object"
},
"outputSchema": {
"properties": {
"flights": {
"items": {
"additionalProperties": false,
"properties": {
"airline": { ... },
"arrival": { ... },
"departure": { ... },
"destination": { ... },
"id": { ... },
"number": { ... },
"origin": { ... }
},
"required": [ "id", "airline", "number", "origin", "destination", "departure", "arrival" ],
"title": "flight",
"type": "object"
},
"type": "array"
}
},
"required": [ "flights" ],
"type": "object"
}
}
The Component Layer
Here’s the Wasm Interface Type (WIT) definition for the flights component from which Toolbelt maps to the JSON above:
interface flights {
record flight {
id: string,
airline: string,
number: string,
origin: string,
destination: string,
departure: string,
arrival: string,
}
get-flight-by-id: func(id: string) -> option<flight>;
get-flights: func() -> list<flight>;
search-flights: func(
origin: string,
destination: string,
departure: string,
arrival: option<string>,
flex: option<u8>,
) -> list<flight>;
}
world flights-world {
include wasi:config/imports@0.2.0-draft;
import rest-client;
export flights;
}
The output-schemas for the other two functions would also include the JSON representation of the flight record. And the hotels component follows the exact same pattern but with its own record type:
interface hotels {
record hotel {
id: string,
name: string,
city: string,
stars: s32,
description: string,
}
get-hotel-by-id: func(id: string) -> option<hotel>;
get-hotels: func() -> list<hotel>;
search-hotels: func(
city: string,
checkin: string,
checkout: string,
flex: option<u8>,
minstars: option<u8>
) -> list<hotel>;
}
world hotels-world {
include wasi:config/imports@0.2.0-draft;
import rest-client;
export hotels;
}
You can see the implementations of these components in the modulewise/demos repository, but you may first want to read the next section to understand how they rely on the rest-client with which they are composed.
The Infrastructure Layer
As shown in the WIT above, these domain components import a rest-client component, whose own WIT definition looks like this:
interface rest-client {
get: func(url: string, headers: list<tuple<string, string>>) -> result<string, string>;
post: func(url: string, headers: list<tuple<string, string>>, body: string) -> result<string, string>;
}
world rest-client-world {
export rest-client;
import wasi:http/outgoing-handler@0.2.3;
}
And here’s the configuration that ties it all together, as provided to the Toolbelt for this demo:
[flights]
uri = "../components/lib/flights.wasm"
expects = ["rest-client"]
exposed = true
[hotels]
uri = "../components/lib/hotels.wasm"
expects = ["rest-client"]
exposed = true
[rest-client]
uri = "../components/lib/rest-client.wasm"
expects = ["wasip2", "http"]
enables = "exposed"
[http]
uri = "wasmtime:http"
enables = "unexposed"
[wasip2]
uri = "wasmtime:wasip2"
enables = "unexposed"
- The flights and hotels components are both exposed so they can be mapped to Tools.
- Each expects the rest-client component, which in turn expects underlying wasmtime features.
- Whereas the rest-client enables the imports of the exposed components directly, the wasmtime features are only made available to unexposed components like rest-client itself.
This highlights an essential feature of the underlying composable-runtime: infrastructure reuse. If you are familiar with dependency injection in the Spring Framework, this is conceptually similar but with a few important distinctions:
- Since WIT is an interface definition language (IDL), these high-level component contracts are not coupled to the programming languages used to implement those components. This same
rest-client
(implemented in rust) can be composed with other components regardless of their implementation language. - Wasm composition bridges imported and exported interfaces through a canonical application binary interface (ABI) rather than injecting instances of types whose API must be available as a library within the consuming code. The implementation of the interface remains encapsulated within the exporting component.
- The binding of exports to imports is happening at deployment time. The developer artifact has no build-time dependencies on infrastructure components, and operations teams can determine what capabilities are available in a given environment. Developers can focus on domain logic, because the actual component they are building cannot directly produce side-effects.
This brings us back to the least-privilege capability theme of the last blog post. Even though the flights and rest-client components are composed together into a single component (no remote procedure calls), the flights component has NO access to the network. Furthermore, the rest-client must be granted those host capabilities at runtime. And since these components only surface interfaces, even the fully composed artifact is very lightweight (flights + rest-client = 216K).
The deny-by-default principle is evident in the configured scopes for the enables property. If one of the exposed components were to request direct access to the HTTP runtime feature, it would fail to load in the Toolbelt. If a more lenient approach is warranted, the enables property can be set to “all”, but in keeping with the theme, its default value is “none”.
You may have noticed in the WIT that the domain components also import a wasi:config
interface. That can provide a base URL property that those components use to construct the URL they pass to the rest-client, otherwise they default to localhost on port 8080. The composable-runtime supports config.[key] = "[value]"
directly in the TOML file, generating and composing a config component with the corresponding target component.
The API Layer
The API layer is intentionally “business as usual” with no awareness that it will be accessed by Agents via Tools.
The underlying flight and hotel REST APIs were built as a Spring Boot app. It serves fake data which it stores in Redis hashes. You can run that app locally by cloning the modulewise/demos repository and following the instructions for the travel demo. The app homepage provides links to Interactive API Documentation and the OpenAPI spec as JSON:
What’s Next?
That wraps up this 3-part series, but stay tuned for future blog posts. A few topics that will likely be covered soon:
- generating polyglot components as adapters for existing systems
- building and configuring interceptors for access control and observability
- authentication, authorization, and credential management
Until then, you can try out the Travel Demo for yourself. And if you do, please provide feedback!