@engini/sdk
v0.19.0
Published
Engini SDK — agent-first ergonomic layer over the Engini Public API
Readme
@engini/sdk
Agent-first TypeScript SDK for the Engini Public API — discover tools, execute them against your connected apps, and wrap them as LLM tool definitions.
npm install @engini/sdkQuickstart
import { Engini } from "@engini/sdk";
const client = new Engini({ apiKey: "eng_…" }); // or set ENGINI_API_KEY
// Discover canonical tool schemas
const tools = await client.tools.get({ applications: ["salesforce"], search: "accounts", limit: 5 });
// Execute a tool against a connection
const [{ connectionId }] = await client.connections.list("salesforce");
const result = await client.tools.execute(
"salesforce_getrecords",
{ sobject: "Account" },
{ connectionId },
);
console.log(result.output);JWT auth is the fallback: new Engini({ token: "<jwt>", companyToken: "<id>" }), or set
ENGINI_API_TOKEN / ENGINI_COMPANY_TOKEN. With an API key the company is bound to the key,
so no company token is needed. Point at another host with new Engini({ …, baseUrl }).
Every request carries a User-Agent identifying this SDK and its runtime (e.g.
engini-sdk-ts/0.13.0 node/20.11.0 darwin-arm64). If you're embedding the SDK inside your own
product, prepend your own token with userAgentPrefix:
const client = new Engini({ apiKey: "eng_…", userAgentPrefix: "my-product/1.4.0" });
// -> "my-product/1.4.0 engini-sdk-ts/0.13.0 node/20.11.0 darwin-arm64"userAgentPrefix only prepends a token — it can't override or remove the SDK's own identity.
Use with an LLM
Provider adapters wrap canonical schemas into vendor tool definitions client-side, with no
vendor SDK dependency. OpenAI is the default; Anthropic is also available.
// Bind applications → connections once, then drive a tool-calling loop
const toolset = client.toolset({
tools: ["salesforce_getrecords"],
connections: { salesforce: "Prod" },
});
const openaiTools = client.provider.wrapTools(await toolset.tools()); // plain OpenAI tool-JSON
// … send openaiTools to the model, get a response …
const results = await toolset.handleToolCalls(llmResponse); // runs the calls, returns resultsclient.mcp inspects and maintains the account's MCP servers — the endpoints Engini
exposes over the Model Context Protocol:
const servers = await client.mcp.list(); // includes deactivated ones
await client.mcp.deactivate(servers[0].mcpServerToken); // reversible; sends only isActive
const tools = await client.mcp.availableTools(token, 2139); // candidates for toolSlugsupdate() replaces connections/workflows rather than merging them, and every
connection entry needs an explicit connectionId (MCP servers are account-wide, so there
is no per-user default). There is no create/delete: those stay in the engini.io UI.
client.opa provisions, inspects, configures and credentials on-premise agents — the
Engini component a customer installs behind their own firewall to reach SQL Server,
Oracle, Priority ERP and file shares:
// Provision one and hand the token to the installer
const agent = await client.opa.create({ name: "Warehouse SQL", pullPeriodSeconds: 30 });
console.log(agent.token); // a credential - store it, do not log it
// Later: is it healthy?
const a = await client.opa.get(agent.agentId);
console.log(a.status, a.version, a.lastSeenAt);
// Change its log level; the agent picks it up on its next poll
const r = await client.opa.update(agent.agentId, { logLevel: "Debug" });
if (!r.appliedToAgent) console.log("stored; the agent is not Online yet");An agent polls Engini, so anything that "talks to" it waits for its next poll. update()
merges — an omitted member leaves that field alone, unlike connections.update(),
which replaces its whole field map. Settings you omit take the server's defaults, and the
minimums come from the connector's own declared metadata rather than the client. Agents are
not connections: client.connections never returns them.
client.toolset(...) builds a local toolset (no I/O until used) or loads a server one via
client.toolset({ toolsetId }).
Files
Tools whose inputSchema marks a field "format": "engini/file" accept files. Wrap a file with
File and pass it as the field value — the SDK base64-encodes it into the
{ base64_content, mime_type, filename } wire shape. A field takes a single file or a list, per
the tool's schema.
import { Engini, File } from "@engini/sdk";
await client.tools.execute(
"gmail_send_mail",
{
to: "[email protected]",
subject: "Reports",
body: "See attached.",
attachmentsarray: [File.fromPath("q3-report.pdf"), File.fromPath("chart.png")],
},
{ connectionId },
);File.fromPath infers the filename + mime type; File.fromBytes(data, { filename, mimeType })
and File.fromBase64(b64, { filename, mimeType }) cover in-memory content.
In the LLM loop an agent can't produce base64, so file fields are presented to it as string fields. Register the files you'll allow and let the model reference one by key:
const results = await toolset.handleToolCalls(llmResponse, {
files: { "q3-report.pdf": File.fromPath("q3-report.pdf") },
});What this adds over the raw REST client
Built on the autogenerated @engini/client, the
SDK adds what the generated client deliberately lacks: typed errors (the EnginiError family),
retry/backoff, auto-pagination, pluggable auth (ApiKeyAuth / BearerAuth), Provider
adapters for OpenAI/Anthropic, and the ergonomic Toolset object.
There's also a machine-first CLI, @engini/cli,
built on this SDK.
Source & docs: https://github.com/engini/engini-sdk
