Documentation Developers Tool SDK

Tool SDK

16 min read·Updated 30 Jul 2026

Tool SDK is the native extension framework for Arble. Write one implementation and Arble makes it available on desktop, mobile, CLI, API, workflows and agents — no adapters, no per-surface reimplementation.

A tool defined with Tool SDK isn’t a plugin bolted onto one client. It’s registered once into Arble’s tool registry, and every runtime that can call a tool — a desktop session, a scheduled workflow, a remote agent — calls it the same way, with the same schema, the same permission gate and the same audit trail.

Why Tool SDK

MCP gives you a protocol for exposing tools to any compatible client. Tool SDK gives you a runtime built specifically for Arble, and handles everything a tool needs to behave well inside an agent loop rather than just respond to a call.

CapabilityWhat Arble handles for you
Typed inputs & outputsEvery call is validated against the declared schema before your code runs, and before the result reaches the agent.
StreamingProgress events and partial results flow over the same channel as the final payload.
AuthenticationAPI keys, OAuth tokens and local credentials resolve through the credential store — your tool never stores a secret.
PermissionsA tool declares what it needs; Arble asks, remembers and enforces it.
MemoryTools read and write the user’s persistent memory store, so results outlive the run that produced them.
ResourcesFiles, images, database handles and URLs pass as typed values, not raw strings.
Cancellation & timeoutsEvery call carries a cancellation signal and a deadline, enforced regardless of what your code does.
Tracing, logging, versioningEvery call is traced end to end and attributed to the tool version that produced it.

None of this is scaffolding you opt into later. It’s the shape a Tool SDK tool has from the moment you write @tool.

Quick example

Here’s a complete tool, start to finish:

PYTHON
from arble import tool, Context

@tool(
    name="weather.current",
    description="Get current conditions for a city.",
    permissions=["network.http"],
)
def current_weather(city: str, units: str = "metric", ctx: Context = None) -> dict:
    response = ctx.http.get(
        "https://api.weather.example/v1/current",
        params={"city": city, "units": units},
    )
    response.raise_for_status()
    data = response.json()
    return {
        "city": city,
        "temperature": data["temp"],
        "condition": data["summary"],
        "units": units,
    }

That’s the whole tool. No manifest file, no separate schema definition, no registration step. From this one function, Arble generates:

ArtifactSource
JSON SchemaInferred from the function signature and type hints
ValidationEnforced on every call, before your code runs
Permission promptBuilt from the permissions list
CLI commandarble tools run weather.current --city Lisbon
REST endpointPOST /v1/tools/weather.current
Streaming endpointPOST /v1/tools/weather.current/stream
Registry entryName, version, owner and permission summary
DocumentationParameter table and examples, from the docstring and type hints

The function signature is the source of truth. Change a parameter’s type and the schema, validation, generated docs and CLI flags all change with it — there’s nothing else to keep in sync.

Supported languages

Tool SDK ships first-party libraries for four languages. Parity across them is a design constraint, not a roadmap item: the same tool built in any of the four gets the same schema inference, the same streaming model and the same registry entry shape.

Python Decorators, type hints, Pydantic models
TypeScript Decorators, Zod schemas, async generators
Go Struct tags, channels for streaming
Rust Attribute macros, Stream for streaming

Choose based on where the tool needs to run and what it needs to link against — not based on what the SDK supports, because all four support the same surface.

Tool lifecycle

A tool moves through nine stages between being an idea and being something another user’s agent can call.

  1. 1DefineWrite the function, its decorator and its docstring.
  2. 2RegisterArble reads the decorator and signature into the local registry.
  3. 3ValidateThe inferred schema is checked for ambiguity before anything runs.
  4. 4TestRun against the test harness with mock resources and permissions.
  5. 5PackageCode, dependencies and metadata bundle into a signed artifact.
  6. 6PublishPushed to a private, organization or public registry.
  7. 7InstallAnother environment pulls the package and its permissions.
  8. 8RunThe agent calls the tool like any other registry entry.
  9. 9ShareShared directly with a teammate or workflow, scoped by name.

Tool anatomy

Every tool is made of the same nine parts, whether it’s three lines or three hundred.

MetadataName, description, owner and version — the identity the registry uses to find the tool
Input schemaThe typed shape of a valid call
PermissionsThe capabilities the tool needs, declared up front
ResourcesFiles, documents or services the tool reads or writes as typed values
ExecutionThe function body — the only part that looks like “your code”
StreamingThe channel for progress events and partial results
Output schemaThe typed shape of a successful result, validated before it reaches the agent
DocumentationGenerated from the docstring, type hints and examples
RegistryEverything above, indexed and queryable by other tools and workflows

Input schemas

A tool’s input schema is inferred from its function signature by default. You widen or constrain it with standard typing constructs — Tool SDK doesn’t introduce its own type language.

Primitivesstr, int, float, bool — map directly to JSON Schema types. Objects — a dataclass, TypedDict or Pydantic model — become nested schema objects with their own validation. Arrays (list[T]) validate every element, not just the container. Enums (Literal[...]) become a closed set of allowed values. Optional fields follow the parameter’s default — present a default and it’s optional, omit one and it’s required.

PYTHON
from pydantic import BaseModel, Field
from typing import Literal

class CreateIssueInput(BaseModel):
    title: str = Field(..., min_length=1, max_length=256)
    body: str = ""
    labels: list[str] = []
    priority: Literal["low", "medium", "high"] = "medium"
    assignee: str | None = None

@tool(name="github.create_issue", permissions=["network.http"])
def create_issue(input: CreateIssueInput, ctx: Context) -> dict:
    ...

Validation runs before your function body executes. A call with priority="urgent" never reaches your code — the caller gets a schema error naming the exact field and constraint that failed.

Output schemas

Output schemas work the same direction in reverse: Arble validates what your function returns before handing it to the agent, so a tool can’t silently return a shape the caller didn’t ask for.

Typed responses. A return type hint becomes the output schema, the same way input types do. Structured outputs. Nested objects and arrays are validated recursively. Streaming. A streaming tool’s output schema describes the final payload; progress and partial events have their own fixed shape. Resources. A tool can return a resource in place of raw data — the agent gets a reference it can pass to another tool, not a blob to parse.

Errors. Raise a typed exception instead of letting one propagate raw, so the agent can reason about the failure instead of seeing an opaque trace.

PYTHON
from arble import ToolError

class RateLimited(ToolError):
    retryable = True

@tool(name="github.create_issue", permissions=["network.http"])
def create_issue(input: CreateIssueInput, ctx: Context) -> dict:
    response = ctx.http.post("https://api.github.com/issues", json=input.dict())
    if response.status_code == 429:
        raise RateLimited("GitHub rate limit hit, retry after backoff")
    response.raise_for_status()
    return response.json()

Resources

A resource is anything a tool reads or writes that isn’t a plain value — Tool SDK treats these as first-class typed inputs and outputs rather than paths or blobs the tool has to manage itself.

Resource typeRepresents
FileA single file on disk, opened and closed by Arble around your call
DirectoryA scoped view of a folder, walked or written within its permission
DatabaseA connection handle to a database Arble has been granted access to
ImageA typed binary resource with format and dimension metadata
DocumentA structured document (PDF, Markdown, Word) with extraction helpers
TableA typed tabular resource — rows and a schema — independent of file format
Web APIA named external endpoint with its authentication already resolved
Binary resourceUntyped bytes, for anything that doesn’t fit a more specific category
Streaming resourceA resource whose content arrives incrementally rather than all at once

Declaring a parameter as Resource[File] instead of str changes what the agent can pass you: a reference Arble already validated and scoped, not a raw path it has to guess is valid.

Permissions

A tool declares what it needs, not how it will use it. Arble is the one that asks, remembers and enforces — the tool’s code never contains a permission check, because if the function body is running, the permission has already been granted.

PYTHON
@tool(
    name="filesystem.write_report",
    permissions=["filesystem.write:~/Reports"],
)
def write_report(path: str, content: str, ctx: Context) -> dict:
    ctx.fs.write(path, content)
    return {"path": path, "bytes": len(content)}
LevelBehavior
Always allowGranted once, applied to every future call without prompting again
Ask every timePrompts on every call, regardless of prior answers
Never allowCalls fail immediately with a permission error, no prompt shown

Grants can be scoped per tool, per workflow or per session, and are inherited down a call chain — a workflow granted network.http doesn’t make a called tool re-prompt, but a tighter scope on the tool is still enforced on top, never loosened by it.

Authentication

Tools that talk to external services need credentials, and Tool SDK resolves them through Arble’s credential store rather than asking you to manage secrets yourself.

API keys

ctx.secrets.get("service_name") resolves from the user’s stored keys, and fails the call with a clear setup error if none exists.

OAuth

Declare an OAuth scope and Arble handles authorization, token storage and refresh — your tool calls ctx.auth.token("github") and gets a valid token.

Bearer tokens

Stored once, injected on request, never logged — the same treatment as an API key.

Environment variables

Resolved through ctx.env, not os.environ, so the tool behaves identically when Arble supplies credentials a different way in production.

Local credentials & device keychain

On desktop, Arble can resolve a credential from the OS keychain instead of its own store.

Secrets

Anything marked as a secret is redacted from logs, traces and error messages automatically.

Never accept a credential as a plain tool parameter. If a caller can pass it, it can leak into a trace or a workflow definition — resolve credentials through ctx, always.

Streaming

A tool that takes more than an instant should stream, not block. Make the function an async generator and yield instead of returning once.

PYTHON
from arble import tool, Context, Progress

@tool(name="repo.clone", permissions=["filesystem.write", "network.http"])
async def clone_repository(url: str, dest: str, ctx: Context):
    yield Progress(message="Resolving repository", percent=0)

    async for received, total in ctx.git.clone(url, dest):
        yield Progress(
            message=f"Receiving objects ({received}/{total})",
            percent=int(received / total * 100),
        )

    yield {"path": dest, "commit": await ctx.git.head(dest)}

Progress updates carry a message and an optional percent, and render as a live status line without polling. Partial outputs let a tool yield pieces of its result as they’re ready. Long-running tasks stay attached to the same call rather than needing a separate status check — the stream is the status. Cancellation arrives on ctx.cancelled, checked cooperatively between yields. Timeouts are enforced by Arble regardless of whether the tool checks cancellation.

Development

arble dev runs a local loop built around fast iteration on a single tool.

Hot reload. Saving the tool file re-registers it within milliseconds. Inspector. A live panel showing the current schema, the last five calls and any validation errors as you type. Schema viewer. Renders the generated JSON Schema exactly as the registry stores it. Permission preview. Shows the exact prompt a user sees on first call. Logs. Structured, per-call logs filterable by tool. Registry. A local-only view matching the shape of the production registry. CLI. Every capability above is also a scriptable command — arble tools inspect, arble tools schema, arble tools logs.

Testing

Tool SDK’s test harness runs a tool without a live Arble session, so tests are fast and don’t depend on network state or a real agent loop.

Unit testing. arble.testing.call(tool, **kwargs) invokes a tool directly against its schema. Mock tools. Any tool your tool calls internally can be swapped for a mock. Replay. Record a real call once and replay it in CI without hitting the live service again. Snapshots. Assert that a tool’s output schema hasn’t changed unexpectedly. Permission simulation. Run a tool as though a permission were denied. Failure simulation. Inject a timeout or malformed response to verify error handling.

PYTHON
from arble.testing import call, deny_permission

def test_create_issue_success():
    result = call(create_issue, input=CreateIssueInput(title="Bug"))
    assert result["title"] == "Bug"

def test_create_issue_without_network_permission():
    with deny_permission("network.http"):
        with pytest.raises(PermissionError):
            call(create_issue, input=CreateIssueInput(title="Bug"))

Publishing

arble tools publish pushes a packaged tool to a registry. Which registry depends on who should be able to install it.

Versioning. Tools follow semantic versioning. A published version is immutable — a fix ships as a new version, never an overwrite. Signing. Every package is signed with the publisher’s key; Arble refuses an unsigned or tampered package on install.

Private registry

  • Visible only to you
  • For tools still in development

Organization registry

  • Visible to your Arble organization
  • Gated by the same role permissions

Public registry

  • Visible to any Arble user
  • Reviewed before listing

Built-in helpers

ctx gives every tool access to a set of helpers so common operations don’t require a new dependency or a hand-rolled client.

HelperProvides
FilesystemScoped read/write/list, respecting the tool’s declared path permission
BrowserA controllable browser context for tools that need to navigate or scrape
DesktopOS-level automation — window focus, screenshots, input simulation
TerminalA sandboxed shell for tools that need to run a command
ClipboardRead and write the system clipboard, gated behind its own permission
NotificationsPost a system notification the user sees outside the session
MemoryRead and write the user’s persistent memory store
SchedulerQueue a future or recurring call to this or another tool
HTTPAn HTTP client with credentials, retries and tracing pre-wired
DatabaseQuery a database Arble has been granted a connection to
EncryptionEncrypt and decrypt values using keys Arble manages
SearchQuery the user’s indexed local content and connected sources

Example tools

Each of these is a normal Tool SDK tool — nothing about them is privileged or built with a different API than the one described on this page.

FilesystemReads, writes and searches files within a granted directory scope
BrowserNavigates, clicks and extracts content from a controlled browser session
WeatherFetches current conditions and forecasts for a named location
GitHubOpens issues, reviews pull requests and reads repository state
SlackPosts messages and reads channel history in connected workspaces
EmailDrafts, sends and searches messages through the connected inbox
CalendarReads availability and creates events on the connected calendar
DatabaseRuns scoped queries against a granted database connection
NotificationsDelivers a system notification outside the current session
CameraCaptures a still image from a paired device’s camera
MicrophoneRecords or transcribes audio from a paired device’s microphone
MemoryReads from and writes to the user’s persistent memory store

Tool registry

Every tool Arble can call — regardless of where it came from — lands in one registry, indexed the same way.

SourceExamples
Built-inFilesystem, terminal, browser, memory
Tool SDKAnything you build and register with @tool
MCPTools exposed by a connected MCP server
DesktopOS-level capabilities on a paired desktop
Remote servicesTools backed by a hosted API rather than local code

The planner that decides which tool to call for a given step reads name, description, input schema and permission requirements — the same four fields regardless of source. Adding a source of tools never means teaching the planner something new, and a tool you publish through Tool SDK is chosen exactly as readily as one Arble shipped with. See MCP server for how connectors join the same registry.

Performance

Cold start. A tool’s first call in a session pays a one-time cost to load its code and resolve permissions; every call after reuses the warm instance. Caching. Declare a result as cacheable with a TTL — @tool(cache_ttl=60) — and repeated identical calls are served from cache. Streaming. Lets Arble start planning the next step before the current one fully completes, when the plan doesn’t depend on the final value. Memory. Write summaries, not raw payloads — a dumped API response bloats every future context that reads it back. Latency. The registry tracks p50/p95 per tool and surfaces regressions before a user reports them. Retries. Mark errors retryable=True and Arble retries with backoff automatically. Timeouts. Set a tool-level default with @tool(timeout=30); callers can request a shorter deadline, never a longer one.

Security

Sandbox

Tool code runs in an isolated process with no ambient access to the filesystem, network or other tools’ data — only what its declared permissions and resources explicitly grant.

Permissions

Every capability is opt-in and declared up front. There is no implicit access a tool can fall back on.

Audit logs

Every call — inputs, the permission grant it ran under, and its outcome — is logged and queryable.

Least privilege

Scope permissions as narrowly as the tool functionally needs — filesystem.write:~/Reports, not filesystem.write:~.

Encryption

Secrets and credentials are encrypted at rest and never appear in plaintext in logs, traces or registry entries.

Revocation

Revoking a permission takes effect on the next call, immediately, with no reinstall or restart required.

Best practices

  1. Keep a tool’s scope narrow — one clear job, not a dozen optional modes.
  2. Name tools namespace.action (github.create_issue), so the registry stays legible at scale.
  3. Write the docstring for the agent, not for yourself — it’s what the planner reads.
  4. Declare the narrowest permission that works, scoped to specific paths or hosts.
  5. Never accept credentials as parameters — resolve them through ctx.secrets or ctx.auth.
  6. Prefer typed models over loose dict parameters — the schema is only as good as the types.
  7. Stream anything that takes more than a couple of seconds.
  8. Check ctx.cancelled between yields in any streaming tool.
  9. Raise typed ToolError subclasses instead of letting exceptions propagate raw.
  10. Mark genuinely retryable errors retryable=True, never ones that write data non-idempotently.
  11. Write memory entries as summaries, not raw dumps.
  12. Version deliberately — a breaking schema change is a major version, not a patch.
  13. Test with permission simulation, not just happy-path calls.
  14. Return resources instead of raw paths or blobs for files, images or documents.
  15. Keep the function body free of permission or credential checks — that’s what the decorator is for.

Reference

  • Decorators@tool, parameters, defaults
  • Schemas — type inference rules, explicit overrides
  • Resources — full resource type catalog and helpers
  • Permissions — scope syntax, grant levels, inheritance
  • StreamingProgress, partial results, cancellation
  • Memory — read/write API, retention, summarization
  • Authenticationctx.secrets, ctx.auth, OAuth setup
  • CLI — full arble tools command reference
  • Publishing — package format, signing, registry configuration
  • Testingarble.testing API reference
  • API — REST and streaming endpoints for published tools

FAQ

Do I need MCP if I’m using Tool SDK?

No. They solve different problems. MCP connects Arble to a server someone else runs; Tool SDK is how you build a tool that runs as a native part of Arble itself. Many setups use both.

Can a Tool SDK tool call an MCP tool?

Yes. From the registry’s perspective they’re both just tools — your function can call one through the same ctx interface it uses for any built-in helper.

What happens if I change a tool’s input schema after publishing?

A backward-compatible change ships as a patch. A breaking change needs a new major version — the old version keeps working for anyone pinned to it.

How does Arble decide which tool to call for a given task?

The planner reads each candidate’s name, description and schema from the registry and picks based on fit, regardless of whether the tool is built-in, SDK or MCP.

Can I test a tool without a full Arble session running?

Yes — the arble.testing harness calls a tool directly against its schema and mocked context, with no live session required.

Do streaming tools need a different permission model?

No. Permissions are declared once on the tool and apply to the whole call, streamed or not.

Where are secrets actually stored?

In Arble’s encrypted credential store, resolved into your tool at call time through ctx.secrets or ctx.auth. Your code never touches the storage layer.

Can I publish a tool privately within my company only?

Yes — the organization registry is visible only to members of your Arble organization.

What’s the difference between permissions and a resource scope?

A permission is a capability (“this tool can write files”); a resource scope narrows where. Most filesystem and network permissions carry a scope as part of the same declaration.

Is there a limit on how long a streaming tool can run?

Every tool has a deadline, defaulting to 30 seconds and overridable with @tool(timeout=...). A streaming tool that needs longer should say so explicitly rather than relying on the default.