Client Tools | Agent Tool Protocol
Skip to main content

Client Tools

Client tools are a powerful feature that allows you to execute functions locally on the client machine while maintaining the full security and orchestration capabilities of ATP. Unlike server-side APIs that run on the server, client tools execute in your application's environment, giving you access to local resources like filesystems, browsers, hardware, and proprietary services.

Why Use Client Tools?

Client tools solve several critical problems:

🔒 Keep Sensitive Operations Local

Execute sensitive operations without sending credentials or data to the server:

  • Access local filesystems and databases
  • Use API keys and credentials that never leave your environment
  • Interact with proprietary internal systems
  • Control browser automation (Playwright, Puppeteer)

🌐 Client-Side Resource Access

Access resources that only exist on the client side:

  • Local file systems and configuration files
  • Hardware devices (cameras, sensors, USB devices)
  • Running applications and processes
  • Browser sessions and cookies
  • Desktop GUI automation

Reduced Latency

Execute operations locally without network round-trips:

  • Faster file operations
  • Immediate access to local data
  • No bandwidth constraints

🛡️ Maintain Security Controls

Client tools fully integrate with ATP's security features:

  • Provenance tracking for data flowing through client tools
  • Security policies applied to client tool results
  • Operation type and sensitivity level metadata
  • Approval workflows for destructive operations

Architecture

Client tools use ATP's pause/resume mechanism to seamlessly integrate local execution into the sandbox workflow:

Key Concepts

  1. Tool Definition: Metadata sent to the server (name, description, schema)
  2. Tool Handler: Function that executes locally on the client
  3. Namespaces: Organize tools into logical groups (e.g., client, playwright, system)
  4. Pause/Resume: Seamless execution flow that feels synchronous in sandbox code

Defining Client Tools

Client tools are defined using the ClientTool interface:

import { 
ToolOperationType,
ToolSensitivityLevel,
type ClientTool
} from '@mondaydotcomorg/atp-protocol';

const readFileToolool: ClientTool = {
// Unique tool name
name: 'readLocalFile',

// Namespace for organization (defaults to 'client')
namespace: 'client',

// Human-readable description
description: 'Read a file from the local filesystem',

// JSON Schema for input validation
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the file to read'
}
},
required: ['path']
},

// Optional: Output schema for documentation
outputSchema: {
type: 'object',
properties: {
content: { type: 'string' },
size: { type: 'number' }
}
},

// Tool metadata for security
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.SENSITIVE,
category: 'filesystem',
tags: ['io', 'files'],
},

// Handler function (executes locally)
handler: async (input: any) => {
const fs = await import('fs/promises');
const content = await fs.readFile(input.path, 'utf-8');
return {
content,
size: content.length,
path: input.path
};
}
};

Registering Client Tools

Client tools are registered when initializing the ATP client:

import { AgentToolProtocolClient } from '@mondaydotcomorg/atp-client';
import { type ClientTool } from '@mondaydotcomorg/atp-protocol';

// Define your tools
const clientTools: ClientTool[] = [
readFileTool,
writeFileTool,
getSystemInfoTool,
];

// Create client with tools
const client = new AgentToolProtocolClient({
baseUrl: 'http://localhost:3333',
serviceProviders: {
tools: clientTools,
},
});

// Initialize - tools are registered with server
await client.init({
name: 'my-app',
version: '1.0.0',
});

During initialization, the tool definitions (metadata) are sent to the server, but the handlers remain on the client side. The server learns about available tools but never gets access to the handler code.

Using Client Tools in Sandbox Code

Once registered, client tools are available in sandbox code under their namespace:

// Execute code that uses client tools
const code = `
// Call client tool (looks just like a server API!)
const file = await api.client.readLocalFile({
path: '/tmp/data.json'
});

// Process the data
const data = JSON.parse(file.content);

// Call another client tool
const sysInfo = await api.system.getSystemInfo();

return {
dataSize: data.length,
hostname: sysInfo.hostname
};
`;

const result = await client.execute(code);
console.log(result.result);

From the perspective of sandbox code, client tools are indistinguishable from server APIs - they're all called via the api object with the same syntax.

Namespaces

Namespaces organize related tools and prevent naming conflicts:

const tools: ClientTool[] = [
// Defaults to 'client' namespace
{
name: 'readFile',
description: 'Read a file',
// ...
},

// Custom namespace for browser automation
{
name: 'navigate',
namespace: 'browser',
description: 'Navigate browser to URL',
// ...
},

// Custom namespace for Playwright
{
name: 'screenshot',
namespace: 'playwright',
description: 'Take a screenshot',
// ...
},
];

Access tools via their namespace in sandbox code:

// Default 'client' namespace
await api.client.readFile({ path: '/tmp/file.txt' });

// Custom namespaces
await api.browser.navigate({ url: 'https://example.com' });
await api.playwright.screenshot({ selector: '#main' });

Tool Metadata and Security

Client tools support the same security metadata as server APIs:

Operation Types

Classify what the tool does:

import { ToolOperationType } from '@mondaydotcomorg/atp-protocol';

const tools = [
{
name: 'getFile',
metadata: {
operationType: ToolOperationType.READ, // Safe read operation
},
// ...
},
{
name: 'saveFile',
metadata: {
operationType: ToolOperationType.WRITE, // Modifies state
},
// ...
},
{
name: 'deleteFile',
metadata: {
operationType: ToolOperationType.DESTRUCTIVE, // Irreversible
requiresApproval: true, // Require user confirmation
},
// ...
},
];

Sensitivity Levels

Indicate data sensitivity:

import { ToolSensitivityLevel } from '@mondaydotcomorg/atp-protocol';

const tools = [
{
name: 'getPublicData',
metadata: {
sensitivityLevel: ToolSensitivityLevel.PUBLIC,
},
// ...
},
{
name: 'getUserData',
metadata: {
sensitivityLevel: ToolSensitivityLevel.SENSITIVE, // PII data
},
// ...
},
];

Required Scopes

Specify required OAuth scopes or permissions:

const tools = [
{
name: 'accessGitHub',
metadata: {
requiredScopes: ['repo', 'read:user'], // GitHub OAuth scopes
},
// ...
},
{
name: 'adminOperation',
metadata: {
requiredPermissions: ['admin', 'write:system'],
},
// ...
},
];

Real-World Examples

Example 1: Local File Operations

import * as fs from 'fs/promises';
import * as path from 'path';

const fileTools: ClientTool[] = [
{
name: 'readFile',
namespace: 'fs',
description: 'Read a file from the local filesystem',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string' },
encoding: {
type: 'string',
enum: ['utf-8', 'base64', 'binary'],
default: 'utf-8'
}
},
required: ['path']
},
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.SENSITIVE,
},
handler: async (input: any) => {
const content = await fs.readFile(input.path, input.encoding || 'utf-8');
return { content, path: input.path };
}
},

{
name: 'writeFile',
namespace: 'fs',
description: 'Write content to a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string' },
content: { type: 'string' },
mode: { type: 'number' }
},
required: ['path', 'content']
},
metadata: {
operationType: ToolOperationType.WRITE,
sensitivityLevel: ToolSensitivityLevel.INTERNAL,
},
handler: async (input: any) => {
await fs.writeFile(input.path, input.content, {
mode: input.mode
});
return { success: true, path: input.path };
}
},

{
name: 'listDirectory',
namespace: 'fs',
description: 'List files in a directory',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string' },
recursive: { type: 'boolean', default: false }
},
required: ['path']
},
metadata: {
operationType: ToolOperationType.READ,
},
handler: async (input: any) => {
const entries = await fs.readdir(input.path, {
withFileTypes: true,
recursive: input.recursive
});
return {
files: entries.map(e => ({
name: e.name,
isDirectory: e.isDirectory(),
isFile: e.isFile()
}))
};
}
}
];

Example 2: System Information

import * as os from 'os';

const systemTools: ClientTool[] = [
{
name: 'getSystemInfo',
namespace: 'system',
description: 'Get detailed system information',
inputSchema: {
type: 'object',
properties: {}
},
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.INTERNAL,
},
handler: async () => {
return {
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
hostname: os.hostname(),
uptime: os.uptime(),
nodeVersion: process.version,
};
}
},

{
name: 'getEnvironmentVar',
namespace: 'system',
description: 'Get an environment variable',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
},
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.SENSITIVE,
},
handler: async (input: any) => {
return {
value: process.env[input.name],
exists: input.name in process.env
};
}
}
];

Example 3: Browser Automation (Playwright)

import { chromium, type Browser, type Page } from 'playwright';

let browser: Browser | null = null;
let page: Page | null = null;

const browserTools: ClientTool[] = [
{
name: 'launch',
namespace: 'playwright',
description: 'Launch a browser instance',
inputSchema: {
type: 'object',
properties: {
headless: { type: 'boolean', default: true }
}
},
metadata: {
operationType: ToolOperationType.WRITE,
},
handler: async (input: any) => {
browser = await chromium.launch({
headless: input.headless ?? true
});
page = await browser.newPage();
return { success: true };
}
},

{
name: 'navigate',
namespace: 'playwright',
description: 'Navigate to a URL',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string' }
},
required: ['url']
},
metadata: {
operationType: ToolOperationType.READ,
},
handler: async (input: any) => {
if (!page) throw new Error('Browser not launched');
await page.goto(input.url);
return {
url: page.url(),
title: await page.title()
};
}
},

{
name: 'screenshot',
namespace: 'playwright',
description: 'Take a screenshot',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string' },
selector: { type: 'string' }
},
required: ['path']
},
metadata: {
operationType: ToolOperationType.WRITE,
},
handler: async (input: any) => {
if (!page) throw new Error('Browser not launched');

if (input.selector) {
const element = await page.locator(input.selector);
await element.screenshot({ path: input.path });
} else {
await page.screenshot({ path: input.path });
}

return { success: true, path: input.path };
}
},

{
name: 'close',
namespace: 'playwright',
description: 'Close the browser',
inputSchema: {
type: 'object',
properties: {}
},
metadata: {
operationType: ToolOperationType.WRITE,
},
handler: async () => {
if (browser) {
await browser.close();
browser = null;
page = null;
}
return { success: true };
}
}
];

Example 4: Database Access

import { createConnection, type Connection } from 'mysql2/promise';

let connection: Connection | null = null;

const dbTools: ClientTool[] = [
{
name: 'connect',
namespace: 'db',
description: 'Connect to local database',
inputSchema: {
type: 'object',
properties: {
host: { type: 'string', default: 'localhost' },
database: { type: 'string' }
},
required: ['database']
},
metadata: {
operationType: ToolOperationType.WRITE,
sensitivityLevel: ToolSensitivityLevel.SENSITIVE,
},
handler: async (input: any) => {
connection = await createConnection({
host: input.host || 'localhost',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: input.database,
});
return { success: true, database: input.database };
}
},

{
name: 'query',
namespace: 'db',
description: 'Execute a SQL query',
inputSchema: {
type: 'object',
properties: {
sql: { type: 'string' },
params: { type: 'array' }
},
required: ['sql']
},
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.SENSITIVE,
},
handler: async (input: any) => {
if (!connection) throw new Error('Not connected to database');
const [rows] = await connection.execute(input.sql, input.params || []);
return { rows };
}
}
];

Provenance Tracking with Client Tools

Client tools fully integrate with ATP's provenance tracking system:

// Enable provenance tracking
const result = await client.execute(code, {
provenanceMode: 'proxy', // Track data lineage
securityPolicies: [preventDataExfiltration],
});

When a client tool returns data, it's automatically tracked:

const code = `
// Data from client tool gets provenance tracking
const file = await api.client.readFile({ path: '/secrets.txt' });

// Provenance: file.content has source = 'client.readFile'

// If you try to send this to an LLM, policy can block it
await llm.call({ prompt: file.content }); // ❌ Blocked by policy
`;

Error Handling

Handle errors gracefully in client tool handlers:

const tool: ClientTool = {
name: 'readFile',
// ...
handler: async (input: any) => {
try {
const content = await fs.readFile(input.path, 'utf-8');
return { success: true, content };
} catch (error: any) {
// Return error info instead of throwing
return {
success: false,
error: error.message,
code: error.code
};
}
}
};

In sandbox code, check the result:

const result = await api.client.readFile({ path: '/tmp/data.json' });

if (!result.success) {
console.error('Failed to read file:', result.error);
return { error: result.error };
}

// Use result.content
const data = JSON.parse(result.content);

Best Practices

✅ Do's

  1. Use Descriptive Names: readFile is better than rf
  2. Include Input Validation: Use JSON Schema to validate inputs
  3. Add Metadata: Always specify operationType and sensitivityLevel
  4. Handle Errors Gracefully: Return error objects instead of throwing
  5. Use Namespaces: Organize related tools together
  6. Document Everything: Clear descriptions help AI agents understand tools
  7. Return Structured Data: Return objects with consistent structure
  8. Keep Handlers Pure: Avoid global state when possible

❌ Don'ts

  1. Don't Expose Dangerous Operations: Without approval requirements
  2. Don't Return Massive Data: Limit response sizes (use pagination)
  3. Don't Use Synchronous APIs: Always use async/await
  4. Don't Ignore Security: Always set appropriate metadata
  5. Don't Leak Credentials: Never return API keys or passwords
  6. Don't Skip Error Handling: Always handle exceptions
  7. Don't Use Ambiguous Names: Be specific about what tools do

Integration with LangChain

Client tools work seamlessly with LangChain:

import { createToolsFromATPClient } from '@mondaydotcomorg/atp-client';
import { ChatOpenAI } from '@langchain/openai';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';

// Create ATP client with client tools
const client = new AgentToolProtocolClient({
baseUrl: 'http://localhost:3333',
serviceProviders: {
tools: clientTools,
},
});

await client.init();

// Convert ATP client to LangChain tools
const tools = createToolsFromATPClient(client);

// Create LangChain agent
const llm = new ChatOpenAI({ model: 'gpt-4' });
const agent = createToolCallingAgent({ llm, tools });
const executor = AgentExecutor.fromAgentAndTools({
agent,
tools,
});

// Execute - agent can use both server APIs and client tools!
const result = await executor.invoke({
input: "Read /tmp/data.json and analyze the data"
});

Advanced: Dynamic Tool Registration

Register tools dynamically after initialization:

// Initial tools
const client = new AgentToolProtocolClient({
baseUrl: 'http://localhost:3333',
serviceProviders: {
tools: [baseTool],
},
});

await client.init();

// Later, add more tools dynamically
const newTool: ClientTool = {
name: 'newOperation',
namespace: 'dynamic',
description: 'A dynamically registered tool',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ result: 'success' })
};

client.provideTools([newTool]);

Complete Example

Here's a complete example putting it all together:

import { AgentToolProtocolClient } from '@mondaydotcomorg/atp-client';
import {
ToolOperationType,
ToolSensitivityLevel,
type ClientTool,
} from '@mondaydotcomorg/atp-protocol';
import * as fs from 'fs/promises';
import * as os from 'os';

// Define client tools
const clientTools: ClientTool[] = [
{
name: 'readFile',
namespace: 'fs',
description: 'Read a file from the local filesystem',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path' }
},
required: ['path']
},
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.SENSITIVE,
},
handler: async (input: any) => {
try {
const content = await fs.readFile(input.path, 'utf-8');
return { success: true, content, size: content.length };
} catch (error: any) {
return { success: false, error: error.message };
}
}
},
{
name: 'getSystemInfo',
namespace: 'system',
description: 'Get system information',
inputSchema: {
type: 'object',
properties: {}
},
metadata: {
operationType: ToolOperationType.READ,
sensitivityLevel: ToolSensitivityLevel.INTERNAL,
},
handler: async () => {
return {
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
memory: os.totalmem(),
};
}
}
];

async function main() {
// Create client with tools
const client = new AgentToolProtocolClient({
baseUrl: 'http://localhost:3333',
serviceProviders: {
tools: clientTools,
},
});

// Initialize
await client.init({ name: 'my-app', version: '1.0.0' });

// Execute code that uses client tools
const code = `
// Use client tools seamlessly
const sysInfo = await api.system.getSystemInfo();
console.log('Running on:', sysInfo.platform, sysInfo.arch);

// Read a local file
const fileResult = await api.fs.readFile({
path: '/tmp/data.json'
});

if (!fileResult.success) {
return { error: fileResult.error };
}

// Process the data
const data = JSON.parse(fileResult.content);

return {
platform: sysInfo.platform,
dataSize: fileResult.size,
recordCount: data.length,
};
`;

const result = await client.execute(code);
console.log('Result:', result.result);
}

main().catch(console.error);

Summary

Client tools are a powerful feature that:

  • ✅ Execute locally on the client machine
  • ✅ Maintain full ATP security and provenance tracking
  • ✅ Use the pause/resume mechanism for seamless execution
  • ✅ Support all security metadata (operation types, sensitivity levels)
  • ✅ Work with any resource accessible to the client
  • ✅ Integrate seamlessly with LangChain and other frameworks

Use client tools whenever you need to access local resources, keep operations private, or integrate with client-side services!

Next Steps

Agent Tool Protocol | ATP - Code Execution for AI Agents