TypeScript SDK (@mantyx/sdk)
The official TypeScript SDK for the MANTYX agent runtime. Define ephemeral agents that mix server-side MANTYX tools with locally-executed tools, run them remotely, and stream events back into your process.
- LLM loop runs on MANTYX (BYOK or platform-hosted models).
- Server-resolved tools (
mantyx,mantyx_plugin,a2a,mcp) execute inside MANTYX — including remote Agent2Agent peers and remote MCP servers. - Client-resolved tools (
local,a2a_local,mcp_local) execute inside your process; the SDK shuttles inputs and outputs over an SSE stream + a tool-result POST. - Tunable provider thinking via
reasoningLevel(string anchors or 0–100). - One-shot runs and multi-turn sessions, both with persisted observability.
- Authenticated with a single workspace API key.
For background, see the agent-runs protocol spec
and the messaging-layer reference in docs/wire-protocol.md
— the latter pins down the exact local_tool_call event shape and the
resolved data structures (a2a_local Agent Card, mcp_local Tool[])
that this SDK ships.
Install
Section titled “Install”npm install @mantyx/sdk zod# or: pnpm add @mantyx/sdk zod# or: yarn add @mantyx/sdk zodRequires Node.js 18.17+ (for fetch and ReadableStream). The SDK depends
on zod (parameter schemas) and @modelcontextprotocol/sdk (the official
MCP TypeScript SDK that powers defineLocalMcp’s stdio + Streamable HTTP
transports). The MCP SDK is loaded lazily — apps that never use
defineLocalMcp don’t pay its startup cost.
Quickstart
Section titled “Quickstart”import { z } from "zod";import fs from "node:fs/promises";import { MantyxClient, defineLocalTool, mantyxTool } from "@mantyx/sdk";
const client = new MantyxClient({ apiKey: process.env.MANTYX_API_KEY!, workspaceSlug: process.env.MANTYX_WORKSPACE_SLUG!, // baseUrl: "https://app.mantyx.io", // override for self-hosted});
const result = await client.runAgent({ systemPrompt: "You are a helpful assistant.", prompt: "Read /etc/hostname and summarize what it says.", tools: [ // Local tool — defined and executed in this process. defineLocalTool({ name: "read_file", description: "Read a file from the local filesystem.", parameters: z.object({ path: z.string() }), execute: async ({ path }) => fs.readFile(path, "utf8"), }), // Reference to an existing MANTYX workspace tool. mantyxTool("tool_cm6abc123"), ],});
console.log(result.text);The SDK opens an SSE stream to MANTYX, listens for local_tool_call events,
runs the matching local handler, and POSTs the result back. The server keeps
running the agent loop until it produces a final reply.
Triggering a persisted MANTYX agent
Section titled “Triggering a persisted MANTYX agent”Pass agentId to run an agent that already exists in your workspace. The
server hydrates the agent’s system prompt, model, and server-side tools
(memory, skills, plugin tools, …) from the Agent row at run time. Anything
you pass in tools is merged on top — typically local tools you want
the agent to be able to call back into for this specific run.
import { defineLocalTool, MantyxClient } from "@mantyx/sdk";import { z } from "zod";
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
const result = await client.runAgent({ agentId: "agent_cm6abc123", // workspace agent id prompt: "Pull the latest deploy logs and summarise them.", tools: [ defineLocalTool({ name: "read_local_file", parameters: z.object({ path: z.string() }), execute: ({ path }) => readFileSync(path, "utf8"), }), ],});console.log(result.text);Notes:
systemPromptbecomes optional whenagentIdis set; if both are sent, the agent’s stored prompt wins.modelIdis also optional: omit it to use the agent’s configured LLM provider, or pass it to override the model for this run.- The API key must be authorized for the agent (an empty
agentIdsallowlist on the key counts as “all agents in the workspace”). Otherwise the call returns403.
The same agentId field works on client.createSession({ ... }) for
multi-turn conversations against a persisted agent.
Agent2Agent delegation
Section titled “Agent2Agent delegation”Hand a turn off to another agent — either a remote peer MANTYX dials directly
(mantyxA2A) or a peer that only the SDK can reach (defineLocalA2A). The
model addresses both with the same { message: string } argument shape, so
an agent prompt that uses one works unchanged with the other.
defineLocalA2A is fully URL-driven: pass the Agent Card URL and the SDK
takes care of the rest — fetching the card on the first run, shipping it
inline as part of the spec, and POSTing JSON-RPC message/send to the
card’s url whenever MANTYX emits a local_tool_call. You don’t write any
A2A code yourself.
import { MantyxClient, defineLocalA2A, mantyxA2A } from "@mantyx/sdk";
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
await client.runAgent({ systemPrompt: "You are a helpful router. Delegate billing to billing_agent.", prompt: "Why was I charged twice last month?", tools: [ // Public peer MANTYX dials directly. mantyxA2A({ name: "billing_agent", description: "Delegate billing questions to the Acme billing agent.", agentCardUrl: "https://billing.acme.com/.well-known/agent-card.json", headers: { Authorization: `Bearer ${process.env.BILLING_TOKEN}` }, }), // Intranet peer the SDK reaches on MANTYX's behalf — URL only. defineLocalA2A({ name: "intranet_hr", agentCardUrl: "https://hr.intranet.acme/.well-known/agent-card.json", headers: { Authorization: `Bearer ${process.env.HR_TOKEN}` }, }), ],});The same headers are sent on both the card fetch and every subsequent
message/send POST, which is typically what intranet peers want. The SDK
caches the resolved card on the tool ref for the duration of the run /
session — re-construct the ref to force a refetch.
Headers and secrets. The
headersyou pass tomantyxA2Aare forwarded as-is. For long-lived credentials, register the peer as a workspaceExternalAgentinstead — those headers support{{secret:NAME}}placeholders. UsemantyxA2Afor short-lived, per-run tokens minted by your application.
Exposing an agent over A2A
Section titled “Exposing an agent over A2A”The inverse direction also works: wrap a MANTYX agent (ephemeral spec or a
persisted agentId) and serve it as an Agent2Agent peer using the official
@a2a-js/sdk library. Other
agents can then discover it at /.well-known/agent-card.json and call
message/send over JSON-RPC — including MANTYX agents elsewhere in your
estate consuming this one via mantyxA2A or defineLocalA2A.
import { MantyxClient } from "@mantyx/sdk";import { serveAgentOverA2A } from "@mantyx/sdk/a2a-server";
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
const handle = await serveAgentOverA2A({ client, agent: { agentId: "agent_cm6abc123" }, // or { systemPrompt, modelId, tools } port: 4000, agentCard: { name: "Acme Support", description: "Customer support questions.", protocolVersion: "0.3.0", version: "1.0.0", url: "http://localhost:4000", skills: [{ id: "support", name: "Support", tags: ["support"] }], capabilities: { streaming: true, pushNotifications: false }, defaultInputModes: ["text"], defaultOutputModes: ["text"], },});
console.log(`A2A peer up on ${handle.url}`);// later: await handle.close();@a2a-js/sdk and express are declared as optional peer dependencies,
so apps that don’t expose an A2A server pay zero bundle cost. Install them
on demand:
npm install @a2a-js/sdk expressEach unique A2A contextId opens a long-lived MANTYX session by default, so
multi-turn message/send calls share conversational history. Pass
conversation: "stateless" to reduce every A2A request to a one-shot
runAgent call.
For lower-level integration (mounting the executor in your own Express /
Fastify / Connect app), @mantyx/sdk/a2a-server also exports a
MantyxAgentExecutor class implementing @a2a-js/sdk/server’s
AgentExecutor interface.
MCP connectors
Section titled “MCP connectors”Expose every tool published by an MCP server to the agent loop in one go, without listing them individually.
import { MantyxClient, mantyxMcp, defineLocalMcp } from "@mantyx/sdk";
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
await client.runAgent({ systemPrompt: "You are a developer assistant with GitHub + filesystem access.", prompt: "Summarize the latest 5 issues on octocat/hello-world.", tools: [ // Remote MCP server (Streamable HTTP) — MANTYX lists the catalog at run // start and proxies every call. Tools surface as `github_<tool>`. mantyxMcp({ name: "github", url: "https://mcp.github.com/v1", headers: { Authorization: `Bearer ${process.env.GH_PAT}` }, toolFilter: ["search_issues", "get_repo"], }), // Local MCP server — fully managed by the SDK. Pass either a // Streamable HTTP `url` *or* an stdio `command`; the SDK opens the // transport, runs `Initialize` + `tools/list`, ships the resolved // catalog inline, and forwards every invocation to `tools/call`. The // model sees `<server>_<tool>` (`fs_read_file`, `fs_list_dir`, …) — // same shape as `mantyxMcp` above.
// (a) Streamable HTTP MCP server. defineLocalMcp({ name: "fs", url: "http://localhost:8080/mcp", headers: { Authorization: `Bearer ${process.env.FS_TOKEN}` }, }),
// (b) stdio MCP server — the SDK spawns the process for you. // defineLocalMcp({ // name: "fs", // command: "mcp-server-filesystem", // args: ["/workspace"], // env: { LOG_LEVEL: "info" }, // }), ],});The MCP transport is opened lazily on the first runAgent / first
session.send, kept warm for subsequent calls within the same run /
session, and closed when the run completes or session.end() is called.
If the MCP server can’t be reached, the SDK throws before submitting the
spec — you get the failure synchronously rather than mid-conversation.
If a remote (kind: "mcp") MCP server is unreachable when the run starts,
MANTYX still exposes a single <server>_unavailable stub so the model can
tell the user why the connector is missing.
Reasoning effort (reasoningLevel)
Section titled “Reasoning effort (reasoningLevel)”Crank up provider thinking on reasoning models without writing provider-specific code:
await client.runAgent({ systemPrompt: "...", prompt: "Plan a multi-week migration.", reasoningLevel: "high", // or 80, etc.});| Form | Values | Notes |
|---|---|---|
| String | "off", "low", "medium", "high" | Snaps to the same anchors the web composer uses (Fast=30, Moderate=50, Smart=80; off=0). |
| Number | integer 0–100 | 0 explicitly disables provider thinking on reasoning models. |
The server maps this onto each LLM’s native dial — reasoning.effort for
OpenAI, thinkingConfig for Gemini, extended-thinking budget for Anthropic.
Non-reasoning models silently ignore it. On sessions, reasoningLevel
inherits from the session and can be overridden per session.send.
Structured output (outputSchema)
Section titled “Structured output (outputSchema)”Constrain the assistant’s final reply to a JSON document matching a
JSON Schema. The wire still ships the reply as text: string, but that
string is guaranteed-parseable JSON. Pair with parseRunOutput for a
typed value with a clean error path:
import { z } from "zod";import { MantyxClient, parseRunOutput } from "@mantyx/sdk";
const Weather = z.object({ city: z.string(), temperature_c: z.number() });const WeatherJsonSchema = { type: "object", properties: { city: { type: "string" }, temperature_c: { type: "number" }, }, required: ["city", "temperature_c"], additionalProperties: false,} as const;
const result = await client.runAgent({ systemPrompt: "Return the weather as JSON.", prompt: "What's the weather in San Francisco right now?", outputSchema: { name: "weather_report", schema: WeatherJsonSchema },});
const report = parseRunOutput(result, (v) => Weather.parse(v));// ^? { city: string; temperature_c: number }The SDK validates name (regex /^[a-zA-Z0-9_-]{1,64}$/), schema shape
(non-array JSON object), and total size (≤ 32 KB) locally so you get a
typed MantyxError up front instead of a server round-trip rejection.
On parse failure (rare; bad model output), parseRunOutput throws
MantyxParseError with the original text preserved.
outputSchema is independent of reasoningLevel — combine them for
deep-reasoning JSON outputs. On sessions it inherits from
createSession({ outputSchema }) and can be overridden per
session.send(prompt, { outputSchema }). See
docs/wire-protocol.md §7 for the full
per-provider mapping.
Picking a model
Section titled “Picking a model”const { models, defaultModelId } = await client.listModels();console.log(models.map((m) => `${m.id}\t${m.label}`).join("\n"));
await client.runAgent({ systemPrompt: "...", prompt: "Hi!", modelId: "platform:cm6abc123", // or "provider:<id>", or "<vendorModelId>"});modelId accepts:
platform:<offeringId>— a platform-hosted model offering.provider:<llmProviderId>— your own BYOK provider’s default model.provider:<llmProviderId>:<vendorModelId>— your provider, override model.<vendorModelId>— bare vendor id; only resolves when one workspace provider can run it.- omitted — workspace default.
Streaming tokens
Section titled “Streaming tokens”for await (const event of client.streamAgent({ systemPrompt: "...", prompt: "Tell me a story.",})) { if (event.type === "assistant_delta") process.stdout.write(event.text); if (event.type === "result") process.stdout.write("\n");}Or use the onAssistantDelta callback on runAgent:
await client.runAgent({ systemPrompt: "...", prompt: "...", onAssistantDelta: (delta) => process.stdout.write(delta),});Multi-turn sessions
Section titled “Multi-turn sessions”Sessions own the agent spec (system prompt, model, tool defs) and the full
message history. Each send is a run scoped to the session.
const session = await client.createSession({ systemPrompt: "You are a friendly REPL.", tools: [ defineLocalTool({ name: "today", description: "Get today's date as ISO 8601.", parameters: z.object({}), execute: () => new Date().toISOString().slice(0, 10), }), ],});
const r1 = await session.send("What day is it?");console.log(r1.text);
const r2 = await session.send("And what about tomorrow?");console.log(r2.text);
await session.end();Tagging runs and sessions with metadata
Section titled “Tagging runs and sessions with metadata”Attach a flat string→string KV to runs and sessions so your team can filter the dashboard by it (Agent runs → “Metadata” filter):
// One-shot runawait client.runAgent({ systemPrompt: "...", prompt: "...", metadata: { customer: "acme", env: "prod", workflow: "support_triage" },});
// Session — every run created via `session.send` inherits these tagsconst session = await client.createSession({ systemPrompt: "...", metadata: { customer: "acme", env: "prod" },});
// Per-message override; merged on top of the session's metadata// (run-level keys win)await session.send("trace this turn", { metadata: { trace_id: "trace_abc" },});Limits enforced server-side: max 16 entries; keys match [A-Za-z0-9._-]{1,64};
values are strings ≤ 256 chars; serialized JSON ≤ 4 KB. Bigger payloads return
400 invalid_request.
Resuming a session from a different process re-binds your local tool
handlers; pass them in via resumeSession:
const session = await client.resumeSession(sessionId, { tools: [ defineLocalTool({ name: "today", description: "Get today's date as ISO 8601.", parameters: z.object({}), execute: () => new Date().toISOString().slice(0, 10), }), ],});API reference
Section titled “API reference”new MantyxClient(options)
Section titled “new MantyxClient(options)”interface MantyxClientOptions { apiKey: string; workspaceSlug: string; baseUrl?: string; // default: https://app.mantyx.io fetch?: typeof fetch; timeoutMs?: number; // default: 60_000}Methods
Section titled “Methods”| Method | Returns |
|---|---|
listModels() | Promise<ModelCatalog> |
runAgent(spec) | Promise<RunResult> |
streamAgent(spec) | AsyncIterable<RunEvent> |
createSession(spec) | Promise<AgentSession> |
resumeSession(sessionId, { tools? }) | Promise<AgentSession> |
endSession(sessionId) | Promise<void> |
cancelRun(runId) | Promise<void> |
| Helper | Use case |
|---|---|
defineLocalTool(opts) | Define a local tool with a Zod parameter schema and handler. |
defineLocalA2A(opts) | Local Agent2Agent peer — pass an agentCardUrl; the SDK fetches the card and speaks message/send for you. |
defineLocalMcp(opts) | Local MCP server — pass either a Streamable HTTP url or an stdio command; the SDK runs Initialize + tools/list + tools/call for you. |
mantyxTool(id) | Reference an existing MANTYX tool by id. |
mantyxPluginTool(name) | Reference an installed platform plugin tool by name. |
mantyxA2A(opts) | Remote Agent2Agent peer reachable from MANTYX (server-resolved). |
mantyxMcp(opts) | Remote MCP server (Streamable HTTP) MANTYX dials and proxies for you. |
Errors
Section titled “Errors”All thrown errors extend MantyxError. Common subclasses:
MantyxAuthError— 401/403 from the server (bad API key, wrong workspace).MantyxNetworkError— transport-layer failures.MantyxRunError— the agent loop terminated with an error.MantyxToolError— a local tool handler threw or timed out.
Examples
Section titled “Examples”Self-contained example projects live under examples/:
examples/oneshot-local-tool— minimal one-shot run with a local tool.examples/session-chat— interactive REPL on top of a session.examples/mixed-tools— combines local, MANTYX, and plugin tools.examples/streaming— token streaming to stdout.examples/list-models— model catalog + pick-and-run.examples/a2a-tools— remote (mantyxA2A) + local (defineLocalA2A) Agent2Agent peers.examples/mcp-tools— remote (mantyxMcp) + local (defineLocalMcp) MCP servers.
Each example is its own project (package.json, tsconfig.json, README.md)
so you can copy any one of them out of the repo and run it standalone.
Wire protocol
Section titled “Wire protocol”This SDK is a thin client over a stable HTTP/SSE protocol. The full
specification ships with the package at
docs/agent-runs-protocol.md. Anyone can
implement a compatible client in another language.
Development
Section titled “Development”pnpm installpnpm test # unit + mock-server testspnpm typecheckpnpm build # emits dist/ (ESM + CJS + d.ts)The SDK has zero internal workspace:* dependencies. pnpm build produces a
self-contained dist/ ready for npm publish.
See CONTRIBUTING.md for the contribution flow and
EXTRACT.md for the (very small) steps to lift this folder
into its own public repository.