Skip to content

Build your first engine

This guide creates a small onboarding capability and publishes the same implementation through application code, the CLI, and MCP. It stays deterministic so you can inspect the harness-independent Invokta boundary before connecting a model or another provider.

Generate the complete standalone version with npm create invokta-engine@latest my-engine and choose the complete profile when prompted, or follow the steps below to assemble the same boundary manually. Non-terminal automation can pass --profile complete; pseudo-TTY automation also passes --yes.

  1. Define the capability

    Create src/engine.ts:

    import { createEngine, defineCapability } from "@invokta/core";
    import { z } from "zod";
    const createWelcomeMessage = defineCapability({
    title: "Create a welcome message",
    description: "Create a welcome message for a new team member.",
    input: z.object({
    name: z.string().trim().min(1),
    }),
    output: z.object({
    message: z.string().min(1),
    }),
    access: "public",
    annotations: {
    readOnly: true,
    destructive: false,
    idempotent: true,
    openWorld: false,
    },
    async run({ input }) {
    return { message: `Welcome, ${input.name}!` };
    },
    });
    export const engine = createEngine({
    name: "hello-engine",
    version: "1.0.0",
    capabilities: {
    "onboarding.create-welcome-message": createWelcomeMessage,
    },
    });

    The key in capabilities is the public capability ID. Consumers do not need to know how the welcome message is produced.

  2. Invoke it directly

    Create src/direct.ts:

    import { engine } from "./engine.js";
    const result = await engine.invoke(
    "onboarding.create-welcome-message",
    { name: "Ada" },
    { source: "direct", principal: null },
    );
    process.stdout.write(`${JSON.stringify(result)}\n`);

    engine.invoke validates the input, applies the access rule, runs the capability, and validates the result before returning it.

  3. Add a CLI composition root

    Create src/cli.ts:

    import { runCli } from "@invokta/cli";
    import { engine } from "./engine.js";
    process.exitCode = await runCli(engine, {
    principal: { id: "local:developer" },
    });

    Build the project, then inspect and run the capability:

    Terminal window
    node dist/cli.js list
    node dist/cli.js describe onboarding.create-welcome-message
    node dist/cli.js run onboarding.create-welcome-message --input '{"name":"Ada"}'
  4. Add an MCP stdio composition root

    Create src/mcp-stdio.ts:

    import { serveMcpStdio } from "@invokta/mcp";
    import { engine } from "./engine.js";
    await serveMcpStdio(engine, {
    principal: { id: "local:mcp-host" },
    });

    Start the compiled file directly with Node. Standard output is reserved for MCP protocol messages.

  5. Inspect interactively with the devtools

    Install @invokta/devtools as a development dependency — projects generated by create-invokta-engine already include it — then serve the built module:

    Terminal window
    npx @invokta/devtools serve dist/engine.js

    The inspector on http://localhost:4100/ lists the capability with its input and output JSON Schemas and invokes it from a schema-seeded editor. The Adapter switch chooses which execution path carries the call — direct, CLI, MCP stdio, or MCP HTTP — and shows what that path exchanged. A live trace records each invocation’s outcome and duration, and test identities mint development Principal values, so access rules can be exercised as different actors.

Next, read how execution channels converge on the same runtime path.