OpenWorkerDeveloper
DocumentationHarness SDK

Tools and MCP

Expose only the tools an agent needs.

The agent definition lists registered names, such as coding.read_file or app.show_preview.

Tool implementationRegistrationExecution
App callback in TypeScript/RustregisterTools / register_toolsApplication process; private callback channel when managed
Native Rust tool crateTools SDK registryCompiled into your chosen host/runner
External MCP serverExplicit server registrationStdio-launched service or configured HTTP endpoint

Tools declare JSON input schemas and return structured results/errors. Cancellation and progress are part of the interface. A callback’s process placement is distinct from its permission classification: registering a tool does not grant unrestricted file, network or screen access.

Use the bundled coding tools for workspace files, search, patches, processes and Git. Use the tools SDK for native tools and the app callback examples below.

Register an application callback

Register tools before creating an agent, then select their qualified names in its definition. These helpers register one status tool; your application owns the UI. Add app.show_status to the agent’s selected tools and use the streamed approval loop for decisions.

TypeScript

import type { Harness } from '@openworkerai/harness-sdk';

export async function registerStatusTool(harness: Harness, showStatus: (text: string) => void) {
  await harness.registerTools({ namespace: 'app', tools: {
    show_status: {
      description: 'Display a status update in this app.',
      inputSchema: { type: 'object', properties: { text: { type: 'string' } },
        required: ['text'], additionalProperties: false },
      execute: async (args, context) => {
        context.signal.throwIfAborted();
        await context.progress('Updating app');
        if (typeof args.text !== 'string') throw new Error('Expected text');
        showStatus(args.text); // Render as escaped text in a graphical UI.
        return { displayed: true };
      },
    },
  } });
}

Rust

use openworker_sdk::{Harness, Tool};
use serde_json::json;

pub async fn register_status_tool(harness: &Harness) -> openworker_sdk::Result<()> {
    harness.register_tools("app", [Tool::new("show_status")
        .description("Display a status update in this app.")
        .input_schema(json!({"type":"object","properties":{"text":{"type":"string"}},
            "required":["text"],"additionalProperties":false}))
        .handler(|args, context| async move {
            context.progress("Updating app", json!({})).await?;
            println!("{}", args["text"].as_str().unwrap_or_default());
            Ok(json!({"displayed":true}))
        })]).await
}

The Rust callback example uses serde_json = "1" in the app’s dependencies. Callbacks use the application’s privileges. For scoped filesystem operations, see the complete file-organizer tools and Node example.

Connect an MCP server

Connect a trusted remote server or launch a local executable before creating the agent. Select only the discovered tools that the application needs.

import type { Harness } from '@openworkerai/harness-sdk';

export async function connectCompanyTools(harness: Harness) {
  await harness.connectMcpServer({
    name: 'company', transport: 'streamable-http',
    url: 'https://mcp.example.com/mcp',
  });
}

// Or launch an app-installed executable:
export async function startLocalTools(harness: Harness) {
  await harness.startMcpServer({
    name: 'local-tools', program: '/absolute/path/to/mcp-server', args: [],
  });
}

These are alternative registrations with application-owned endpoints/programs. Choose installed qualified tool names in the agent definition after discovery. A remote MCP server executes its tools remotely; a local MCP executable runs with its host process permissions. See the SDK reference for connection options and OpenShell for the separate execution boundary.

Tool names and directory scope

Register/select files.move_file; the model-facing alias is files__move_file. Current events and approval requests can contain that alias. Use your registered mapping for UI labels and preserve original IDs in saved event data.

workspace supplies a working directory and scope for supported built-in path checks/backend mounts. Arbitrary app callbacks can access everything their app can access. Each file handler must constrain both source and destination, reject traversal/absolute paths and handle symlinks. The bundled demo does this for an app-owned directory without concurrent untrusted writers. Path checks are not kernel isolation; use an isolated execution backend for hostile code. Configuring OpenShell does not move arbitrary Node callbacks into its sandbox.

For external services, keep endpoints and credentials in trusted registration configuration. Guardrails may inspect selected arguments/results, and authorization still runs before execution. See Guardrails guide.

Correlate tool activity in your UI

Each application callback receives a call ID: context.callId in TypeScript or context.call_id in Rust. The tool-start and tool-finish event summaries carry the same ID (summary.callId in TypeScript; RunEvent::ToolStarted and RunEvent::ToolFinished have call_id in Rust). The raw journal retains event.data.call_id. Providers can reuse call IDs in later turns: include the turn ID (context.turnId / context.turn_id, and event.turn_id or the Rust tool event’s turn_id) when correlating activity.

The harness records the start before running a tool. Your application may receive the callback before it consumes that journal event, however, because the two channels are independent. Create or update the same UI entry using the session ID, turn ID and call ID from either channel. Do not wait inside a callback for the UI to read the start event, and do not let a late start reset a completed entry. This also works when reading saved events after the run has finished.

A provider response that reuses a call ID from retained model context is rejected before dispatching that new call. Earlier completed actions remain completed; a failed later turn does not undo them. The correlation fields help your UI preserve that distinction.

Typed start/finish summaries omit arguments. They are supplied directly to the callback and available in raw tool_started data; retain them for the finished UI row. Support missing/withheld content and display only appropriate fields.

// Inside an application's run event loop:
if (item.event.type === 'tool_started') {
  const data = item.event.data as Record<string, unknown>;
  updateActivity({ sessionId: item.sessionId, turnId: item.event.turn_id,
    callId: data.call_id, tool: data.name, arguments: data.arguments });
}

updateActivity is application UI code. context.progress(message, data) supplies intermediate updates without introducing new polling.

Images from application callbacks

An application callback can explicitly return one image alongside JSON metadata:

import { ToolImageResult } from '@openworkerai/harness-sdk';
// Inside execute(): capture only the app-authorized surface.
return new ToolImageResult(
  { width: 960, height: 600, scope: 'game-preview' },
  { mimeType: 'image/jpeg', base64: screenshotBytes.toString('base64') },
);

Requires a host advertising application_tool_images and a vision-capable model. JPEG/PNG only, one image, at most 768,000 base64 characters; ordinary metadata retains its 128,000-character bound. The host validates image signatures. Only the latest image per session is kept ephemerally for up to five minutes; durable events and history contain a reference, not pixels. Older images must be recaptured when needed. The provider receives pixels and applies its own retention rules. This API does not grant capture permission or make callbacks sandboxed: applications must authorize and constrain what they capture. Plain JSON return values are unchanged.

Display tool activity

On tool_started and tool_finished, summary.tool keeps the model alias (for example, files__move_file). summary.qualifiedName identifies the registered tool (files.move_file), and summary.modelAlias explicitly records the alias. Names come from the installed registry, including custom MCP aliases. Older hosts may omit the additional fields.

for await (const item of run.events()) {
  const step = item.summary;
  if (step?.type === 'tool_started') {
    console.log(step.qualifiedName ?? step.tool, step.arguments);
    // Correlate with the callback using item.sessionId,
    // item.event.turn_id and step.callId.
  }
}

Start arguments come from the released journal event, after applicable input checks and transformations. They can be absent on older hosts or explicitly null when withheld. Finished events do not repeat them. Treat arguments as potentially sensitive when choosing what to display or log.

Application callbacks receive context.qualifiedName, context.modelAlias and context.callId as well as session/turn IDs. Approval interruptions expose the same identities as qualified_name and model_alias, retaining the existing tool field. A callback can run before your UI consumes its recorded start event; use these IDs to reconcile activity, rather than assuming delivery order.

Search guides, examples and deployment instructions.