Complex Apps
This guide shows one project shape for a larger app. Keep domain and infrastructure code separate from Ohtools composition, define tools close to application services, then declare hierarchy in one place.
When to use this: reach for this layout when a tool app has multiple domains, external services, tests, and both CLI and MCP stdio exposure.
src/
domain/
application/
infrastructure/
tools/
graph-tools.ts
graph-hierarchy.ts
tooling/
ohtools-store.ts
Tool modules export reusable definitions.
// docs-snippet: skip
import { defineTool, jsonSchema } from "@bosun-sh/ohtools";
import type { GraphService } from "../application/graph-service";
export function graphBfsTool(service: GraphService) {
return defineTool({
id: "graph.traversal.bfs",
description: "Traverse a graph in breadth-first order.",
input: jsonSchema<{ graphId: string; start: string }>({
type: "object",
properties: {
graphId: { type: "string" },
start: { type: "string" }
},
required: ["graphId", "start"]
}),
run: (input) => service.breadthFirstTraversal(input)
});
}
Hierarchy modules compose exact-ID definitions without moving tool ownership. Shorthand group tools still use relative IDs and are prefixed by their group.
// docs-snippet: skip
import { defineGroup } from "@bosun-sh/ohtools";
import type { GraphService } from "../application/graph-service";
import { graphBfsTool } from "./graph-tools";
export function graphHierarchy(service: GraphService) {
return defineGroup({ id: "graph", description: "Graph tools." }, (graph) =>
graph.group(
defineGroup({ id: "graph.traversal", description: "Traversal tools." }, (traversal) =>
traversal.tool(graphBfsTool(service))
)
)
);
}
The app store wires services, groups, adapters, and runtime access.
// docs-snippet: skip
import { Ohtools } from "@bosun-sh/ohtools";
import { mcpAdapter } from "@bosun-sh/ohtools/adapters/mcp";
import { GraphService } from "../application/graph-service";
import { graphHierarchy } from "../tools/graph-hierarchy";
const service = new GraphService();
export const app = new Ohtools({ name: "graph-tools" })
.group(graphHierarchy(service))
.adapter(mcpAdapter({ stdio: true }));
Use runtime.runTool when the caller has the definition object. It infers the
input and output types from the tool.
// docs-snippet: skip
import { Effect } from "effect";
import { graphBfsTool } from "../tools/graph-tools";
import { app } from "../tooling/ohtools-store";
const bfs = graphBfsTool(service);
const result = await Effect.runPromise(
app.runtime().runTool(bfs, { graphId: "city-grid", start: "warehouse" })
);
result.output.order;