Server4Agent

Interfaces

SDKs

Typed clients for the code around your agent: your backend, a CI job, a webhook receiver, an admin script. The hand-written JavaScript/TypeScript and Python clients share the same methods and shapes; a typed Go client is also generated from the OpenAPI spec.

Not for your agent's tool-calling loop. If your agent decides on its own when to provision a server or run a command, point it at the MCP server directly, it already speaks tool-calling and needs none of these SDKs. These clients are for the surrounding system: the parts of your stack that call the API on a schedule, in response to a webhook, or from your own product logic.

Every example below switches together, and stays put on your next visit.

Examples in

Install

The JS client is ESM, Node 18+, and ships its own types. The Python client is sync and async, supports 3.9+, and depends only on httpx. The Go client is generated from the OpenAPI spec and needs Go 1.21+.

shell
npm install @server4agent/sdk

Configure the client

The Python and JavaScript clients read your key from the SERVER4AGENT_API_KEYenvironment variable if you don't pass one explicitly, and let you set a per-request timeout and the retry count. The Go client authenticates differently: put the key on a context value and pass that context to every call. This key is a sk_live_ secret: keep it server-side, never ship it in a browser bundle.

client.ts
import { Server4Agent } from "@server4agent/sdk";

// With no arguments, reads SERVER4AGENT_API_KEY from the environment.
const client = new Server4Agent();

// Or pass the key and tune the transport explicitly.
const tuned = new Server4Agent({
  apiKey: process.env.SERVER4AGENT_API_KEY,
  timeoutMs: 30_000, // per-request timeout (default 60_000)
  maxRetries: 4,     // retries on 429/5xx/network (default 2)
});

Quickstart

Provision a server, hand the on-server agent a goal, and wait for a live URL. Every method maps 1:1 to a REST endpoint; the SDK just gives it a type.

quickstart.ts
import { Server4Agent } from "@server4agent/sdk";

const client = new Server4Agent();

// create() resolves to a handle you can act on directly.
const server = await client.servers.create({ tier: "small" });

// Hand the on-server agent a goal and block until it's live.
const build = await server.builds.start("a FastAPI todo API with a small web UI");
await build.wait({ onProgress: (b) => console.log("build:", b.status) });

console.log("live at", build.url);

Handles

create() and get() return rich handles: data records you can also act on. A Server handle exposes sub-resources (tasks, builds, files, projects) already scoped to it, so you never pass its id twice. Every method also has a flat form on the client if you'd rather pass ids around.

handles.ts
// create() and get() both resolve to a Server handle.
const server = await client.servers.get("srv_abc");

// Its sub-resources are already scoped to it: no id to pass again.
await server.tasks.create("Summarize today's sales into report.md");
await server.files.write("notes.txt", "hello");
await server.exec("ls -la");
await server.deploy();
await server.refresh(); // re-fetch the record in place

// Prefer passing ids around? The flat API takes a server id up front.
await client.tasks.create(server.id, "Same task, id passed explicitly.");
await client.files.write(server.id, "notes.txt", "hello");

Servers

A server is a sized compute box. tier picks the size; the task field is just a human-readable label. Create, list, get, control the lifecycle, and delete.

servers.ts
// tier sizes the box: "small" | "medium" | "large". task is a label.
const server = await client.servers.create({ tier: "medium", task: "data pipeline" });

const all = await client.servers.list();
const one = await client.servers.get(server.id);

// Lifecycle controls.
await server.stop();
await server.start();
await server.restart();

// Rename, re-fetch, tear down.
await server.update({ task: "renamed pipeline" });
await server.refresh();
await server.delete();

Run commands

exec() runs a shell command on the server and resolves to its stdout, handy for installing packages, running scripts, or inspecting the workspace.

exec.ts
// exec() runs a shell command on the server and resolves to stdout.
await server.exec("pip install httpx");
const out = await server.exec("python -c 'import sys; print(sys.version)'");
console.log(out);

Files

Read, write, list, and delete files in the server's workspace. The workspace is durable: files and installed dependencies persist between tasks and builds.

files.ts
// The workspace is durable: files and installed deps survive between tasks.
await server.files.write("app.py", "print('hello from the server')");

const listing = await server.files.list();     // string[] of paths
const content = await server.files.read("app.py");
await server.files.delete("app.py");

Tasks

A task hands the on-server agent a single goal. wait() polls until it reaches a terminal state; pass a progress callback (onProgress in JS, on_poll in Python) to stream status to a log or UI. A task can finish as completed, failed, cancelled, or awaiting_input when the agent needs a decision from you.

tasks.ts
// A task hands the on-server agent a goal. wait() polls until it settles.
const task = await server.tasks.create("Scrape today's HN front page to hn.json.");
await task.wait({ onProgress: (t) => console.log("task:", t.status) });

// Terminal states: completed, failed, cancelled, awaiting_input.
if (task.status === "completed") {
  console.log(task.result);
} else if (task.status === "awaiting_input") {
  // The agent needs a decision from you before it can continue.
  console.log("agent asks:", task.question);
}

Builds

For a whole project, builds.start() runs an ordered pipeline (plan, scaffold, implement, install, build, deploy) on the server. The build handle carries per-step status, the produced artifacts, and the final live url.

builds.ts
// A build runs an ordered pipeline: plan, scaffold, implement, install,
// build, deploy. Stream step-by-step progress through onProgress.
const build = await server.builds.start("Build a SaaS dashboard with auth and deploy it.");

await build.wait({
  timeoutMs: 900_000,
  onProgress: (b) => {
    const step = b.steps.find((s) => s.status === "running");
    console.log("building:", step?.label ?? b.status);
  },
});

console.log("live at", build.url);
console.log("artifacts:", build.artifacts); // e.g. ["app/page.tsx", "package.json"]

Projects & deploys

A project is a workspace and app inside a server, with its own slug and public URL. New projects are private: their url stays null until you flip visibility to public, and the slug and URL survive visibility toggles. Start from a template (list them with templates.list()) and mark short-lived ones ephemeral so they clean up.

projects.ts
// A project is a workspace + app inside a server. New projects are PRIVATE:
// url is null until you publish. Start from a template if you like.
const templates = await client.templates.list();
const project = await server.projects.create({
  name: "status-page",
  template: "dashboard",
  // visibility defaults to "private"; lifecycle defaults to "persistent".
});

console.log(project.url); // null while private

// Publish it. The slug and URL stay stable across visibility toggles.
await project.update({ visibility: "public" });
await project.refresh();
console.log(project.url); // https://<slug>.apps.server4agent.com

// Deploy the current workspace to a live URL, list deployments.
const url = await server.deploy();
const history = await server.deployments();

// Ephemeral projects clean up with cleanup(); persistent ones with delete().
await project.cleanup();

Errors & retries

Every failure is a Server4AgentError (JS) or subclass of it (Python), so you can catch broadly or by type: NotFoundError, RateLimitError, APIStatusError. Each carries the HTTP status, a machine-readable code, the message, and a request id to quote to support. Retryable failures (429, 5xx, network blips) are retried automatically with exponential backoff; tune the count with maxRetries / max_retries.

errors.ts
import {
  Server4AgentError,
  NotFoundError,
  RateLimitError,
  APIStatusError,
} from "@server4agent/sdk";

try {
  await client.servers.get("srv_does_not_exist");
} catch (err) {
  if (err instanceof NotFoundError) {
    // 404
  } else if (err instanceof RateLimitError) {
    // 429, already retried; quote err.requestId to support
  } else if (err instanceof APIStatusError) {
    console.error(err.status, err.code, err.message, err.requestId);
  } else if (err instanceof Server4AgentError) {
    // anything else from the SDK (timeouts, network)
  }
}

Async (Python)

Python ships a second client, AsyncServer4Agent, that mirrors the sync one method-for-method: same names, same shapes, every call awaited. The JS client is already promise-based, so there's nothing extra to import.

note.ts
// The JS client is already promise-based: every method returns a Promise,
// so there's no separate async client. Just await.
const server = await client.servers.create({ tier: "small" });
const task = await server.tasks.create("Summarize today's top HN posts to summary.md");
await task.wait();
console.log(task.result);

Verifying webhooks

The Python and JavaScript clients export a verify_webhook_signature /verifyWebhookSignature helper that checks the Server4Agent-Signatureheader against your webhook's secret (from Go, verify it yourself). Always verify against the raw request body, before you parse it as JSON. See Webhooks for the event catalog and how to subscribe.

verify.ts
import { verifyWebhookSignature } from "@server4agent/sdk";

// req.text() must be the RAW body. Verify before you JSON.parse it:
// re-serializing changes the bytes and breaks the signature.
const raw = await req.text();
const ok = await verifyWebhookSignature(
  process.env.SERVER4AGENT_WEBHOOK_SECRET!,
  raw,
  req.headers.get("server4agent-signature"),
);
if (!ok) return new Response("invalid signature", { status: 401 });

const event = JSON.parse(raw);

Recipes

Ephemeral preview environments in CI

Provision a real, disposable server per pull request, deploy the branch to it, and clean it up when the PR closes.

preview-env.ts
// Run from CI on every PR: give it a real, disposable URL, then tear it
// down when the PR closes. lifecycle "ephemeral" marks it for cleanup.
import { Server4Agent } from "@server4agent/sdk";

const client = new Server4Agent();
const pr = process.env.PR_NUMBER!;

const server = await client.servers.create({ task: `pr-${pr}` });
const project = await server.projects.create({
  name: `pr-${pr}`,
  visibility: "public",
  lifecycle: "ephemeral",
});

const build = await server.builds.start("Install deps, build the app, and deploy it.");
await build.wait();

console.log(`Preview: ${project.url}`);
// On PR close: await project.cleanup();

A webhook receiver that reacts to deployments

Subscribe once (see Webhooks), then verify and act on each delivery.

webhook-receiver.ts
import express from "express";
import { verifyWebhookSignature } from "@server4agent/sdk";

const app = express();

app.post("/hooks/server4agent", express.text({ type: "*/*" }), async (req, res) => {
  // req.body must be the raw string; verify before you JSON.parse it.
  const ok = await verifyWebhookSignature(
    process.env.SERVER4AGENT_WEBHOOK_SECRET!,
    req.body,
    req.header("Server4Agent-Signature"),
  );
  if (!ok) return res.status(401).end();

  const event = JSON.parse(req.body);
  if (event.event === "deployment.live") {
    await notifySlack(`Deployed: ${event.url}`);
  }
  res.status(200).end();
});

Per-customer workspaces from your own product

If a customer action should provision a real environment, that's your backend calling the SDK directly, no agent tool-calling loop involved.

provision.ts
// Your product's backend, not an agent: a customer submits a request, your
// own business logic decides what to build, and you provision a real
// workspace for them on demand.
import { Server4Agent } from "@server4agent/sdk";

const client = new Server4Agent();

export async function provisionForCustomer(customerId: string, goal: string) {
  const server = await client.servers.create({ task: `customer-${customerId}` });
  const project = await server.projects.create({
    name: `${customerId}-workspace`,
    visibility: "public",
    template: "dashboard",
  });
  await server.tasks.create(goal);
  return project.url;
}

Scoping a key before handing it to another tool

Mint a key restricted to one server (or project) instead of sharing your account-wide key.

scoped-key.ts
// Hand a support tool a key that can only touch one customer's server,
// not your whole account.
const supportKey = await client.keys.create("support-tool");
await client.keys.updateScope(supportKey.id, {
  allowedServerIds: [server.id],
});

// supportKey.key is the plaintext, shown once, here. Hand it to the tool
// and store the id if you'll need to revoke it later.
// await client.keys.revoke(supportKey.id);

About the Go client

Flip the language switch to Go and every example above rewrites itself in Go. A few conventions are worth calling out, because the Go client is generated from the OpenAPI spec rather than hand-written like the Python and JS clients:

  • Auth is a context value, not a constructor argument, so set your sk_live_ key as a bearer token and pass the context to every call.
  • There are no handles. Every call is a per-resource service (client.ServersAPI, client.TasksAPI, …) that takes the ids explicitly and returns the model, the *http.Response, and an error.
  • No wait(): poll GetTask / GetBuild until the status is terminal.
  • Optional fields are pointers; the server4agent.PtrString helpers and typed enum constants (SERVERTIER_SMALL) fill them in.
main.go
package main

import (
	"context"
	"fmt"
	"os"

	server4agent "github.com/Server4Agent/server4agent-go"
)

func main() {
	client := server4agent.NewAPIClient(server4agent.NewConfiguration())

	// Your sk_live_ key as a bearer token.
	ctx := context.WithValue(context.Background(),
		server4agent.ContextAccessToken, os.Getenv("SERVER4AGENT_API_KEY"))

	tier := server4agent.SERVERTIER_SMALL
	server, _, err := client.ServersAPI.CreateServer(ctx).
		ServerCreate(server4agent.ServerCreate{Tier: &tier}).
		Execute()
	if err != nil {
		panic(err)
	}

	fmt.Println("server", server.GetId(), server.GetStatus())
}

Method and model docs are generated in the server4agent-go repo, and every call maps to the API reference.

Source

The SDKs are open source, one public repo per language: server4agent-python, server4agent-js, and server4agent-go. The Python and JS repos ship runnable examples and a full test suite. Every method wraps one of the endpoints on the REST API page.

Next: Webhooks

Connect an agent

Create a key, paste the MCP config, and give your agent a persistent server it can build on.

MCP setup