SoulFire LogoSoulFire
SDK

TypeScript SDK

Connect to SoulFire from TypeScript with Effect or Promises.

The root @soulfiremc/sdk export uses Effect. It represents connections as scoped resources, streams as Stream, errors as tagged values, and shared clients as Layer.

Use @soulfiremc/sdk/promise with Promises and async iterables. Both entry points provide the same SoulFire features.

Install

bun add @soulfiremc/sdk effect @effect/platform

Effect and @effect/platform are peer dependencies. Use one compatible Effect runtime in the application.

Run a managed local server on Node.js

Local JVM download and process management use explicit Node.js entry points and require Node.js 22 or newer. The Promise example creates or reuses an instance and an offline bot. Run an offline-mode Minecraft server at 127.0.0.1:25565 before starting the bot. install() starts SoulFire locally, so no API token or preconfigured IDs are needed.

import { AccountTypeCredentials } from "@soulfiremc/sdk";
import { SoulFire } from "@soulfiremc/sdk/node/promise";

await using soulfire = await SoulFire.install();
const name = "Hello World";
const existing = (await soulfire.instances()).find(
  (item) => item.friendlyName === name,
);
const instance = existing
  ? soulfire.instance(existing.id)
  : await soulfire.createInstance(name);

const username = "SoulFireBot";
let botId = (await instance.bots()).find(
  (item) => item.accountName === username,
)?.profileId;
if (!botId) {
  for await (const result of instance.loginCredentials({
    service: AccountTypeCredentials.OFFLINE,
    payload: [username],
  })) {
    if (
      result.data.case === "oneSuccess" &&
      result.data.value.account
    ) {
      await instance.addAccounts([result.data.value.account]);
      botId = result.data.value.account.profileId;
    }
  }
}
if (!botId) throw new Error("Could not create an offline bot");

const bot = instance.bot(botId);
await bot.start();
await bot.waitForOnline();
await bot.chat.send("Hello from SoulFire");

To manage a local server with Effect, import SoulFire from @soulfiremc/sdk/node and run SoulFire.install() inside an Effect.scoped program.

The universal @soulfiremc/sdk and @soulfiremc/sdk/promise exports never load Node.js process, filesystem, or archive modules.

Connect and send chat

import { Effect } from "effect";
import { SoulFire } from "@soulfiremc/sdk";

const program = Effect.scoped(
  Effect.gen(function* () {
    const soulfire = yield* SoulFire.connect({
      baseUrl: "https://soulfire.example.com",
      token: process.env.SOULFIRE_TOKEN,
    });

    const bot = soulfire.instance("instance-uuid").bot("bot-uuid");
    yield* bot.start();
    yield* bot.chat.send("Hello from SoulFire");
  }),
);

await Effect.runPromise(program);

The connection handshake checks server compatibility and required capabilities before the client becomes available.

Use Effect Platform

@effect/platform is the general compatibility layer for Effect. SoulFire can consume its portable HttpClient service instead of relying on a global fetch:

const client = yield* SoulFire.connectWithHttpClient({
  baseUrl: "https://soulfire.example.com",
  token,
});

Use the runtime entry point when SoulFire provides the HTTP layer:

  • @soulfiremc/sdk/node uses @effect/platform-node/NodeHttpClient.layerUndici.
  • @soulfiremc/sdk/browser uses @effect/platform/FetchHttpClient.
  • @soulfiremc/sdk/bun uses the same portable fetch layer over Bun's native Fetch implementation.

The universal entry point keeps HttpClient as an application-provided service. This is useful in tests, workers, Deno, and applications with a custom client policy. Runtime-specific filesystem, process, socket, and worker services can come from @effect/platform-browser, @effect/platform-node, or @effect/platform-bun when the application needs them.

For custom integrations, @soulfiremc/sdk/platform exports makeEffectHttpClientFetch.

Stream bot events

yield* bot.events().pipe(
  Stream.runForEach((event) =>
    Effect.logInfo("Bot event", { kind: event.event.case })
  ),
);

The stream stays associated with the configured bot across Minecraft reconnects. Use bot.observe() when the application wants synchronized state instead of processing every delta itself.

Interact with blocks and beds

Use interactBlock when the intent is to activate a block rather than place against it. SoulFire checks that the target is loaded and reachable.

yield* bot.interactBlock({
  position: buttonPosition,
  face: BlockFace.NORTH,
  hand: Hand.MAIN,
});

yield* bot.sleep({
  bed: bedPosition,
  hand: Hand.MAIN,
});

yield* bot.wake();

sleep completes after the server confirms the sleeping state. Invalid targets, daytime restrictions, occupied beds, and rejected interactions surface as typed action failures.

Run concurrent fleet work

Effect controls concurrency and interruption without losing error types:

yield* Effect.forEach(
  botIds,
  (botId) =>
    soulfire
      .instance(instanceId)
      .bot(botId)
      .chat
      .send("Ready"),
  { concurrency: 20, discard: true },
);

The Promise facade works with ordinary Promise.all when that is a better fit for the application.

Promise streams are async iterables by default. Convert one to a Web stream when a browser, worker, Deno, or Node.js pipeline expects it:

import { toReadableStream } from "@soulfiremc/sdk/promise";

const events = toReadableStream(bot.events());

The adapter respects Web Stream backpressure and closes the underlying ConnectRPC iterator when the stream is cancelled.

Execute production tasks

Recipe inspection and execution are available through bot.recipes:

const recipes = yield* bot.recipes.list({
  resultItemId: "minecraft:iron_ingot",
});

const task = yield* bot.recipes.smelt(
  { itemIds: ["minecraft:raw_iron"] },
  8,
  {
    fuel: { tags: ["minecraft:coals"] },
    station: furnacePosition,
  },
);

yield* task.result();

Crafting and smelting run on the SoulFire server as resource-aware tasks. Interrupt the local stream for a call-owned task. Keep the task handle when the task must survive the SDK process.

Provide a shared layer

Define a program dependency

import { Effect } from "effect";
import { SoulFireService } from "@soulfiremc/sdk";

const announce = Effect.gen(function* () {
  const soulfire = yield* SoulFireService;
  yield* soulfire
    .instance(instanceId)
    .bot(botId)
    .chat
    .send("Ready");
});

Provide the live client

await Effect.runPromise(
  announce.pipe(
    Effect.provide(SoulFire.layer({ baseUrl, token })),
  ),
);

Replace that layer in tests to run application logic without a network connection.

Use generated protocol definitions

High-level modules cover normal application work. Generated protobuf services remain available for advanced protocol access:

import {
  InstanceService,
} from "@soulfiremc/sdk/generated/soulfire/instance_pb";

const client = soulfire.service(InstanceService);

Generated clients expose the wire contract directly. They do not add the validation, task ownership, cleanup, or stable error behavior of the high-level SDK.

Continue with durable tasks or plugin-defined APIs.

How is this page?

Last updated on

On this page