Agent skill
claude-agent-ts-sdk
Build Claude agents using TypeScript with the @anthropic-ai/claude-agent-sdk. Use this skill when implementing conversational agents, building tools for agents, setting up streaming responses, or debugging agent implementations. Covers the tool wrapping pattern, SDK initialization, agent architecture, and best practices.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development-tools/claude-agent-ts-sdk
SKILL.md
Claude Agent TypeScript SDK
Overview
Build production-ready Claude agents using TypeScript and the @anthropic-ai/claude-agent-sdk. This skill provides battle-tested patterns from successful implementations, focusing on the tool wrapping approach (not full MCP servers) for creating modular, composable agent tools.
When to Use This Skill
Use this skill when:
- Implementing a new Claude agent in TypeScript
- Creating tools for agent use
- Setting up streaming agent responses
- Debugging agent SDK implementations
- Converting between MCP server and tool wrapping approaches
- Building CLI agents, web server agents, or plugin-based agents
- Working with subprocess tools, file system tools, or API wrappers
Authentication
The SDK automatically uses Claude Code's authentication - no setup required. Your agent works seamlessly within Claude Code without any authentication configuration.
Core Architecture Patterns
Pattern 1: CLI/Specialized Agent
Best for: Command-line tools, specialized workflows, batch processing
// src/agent.ts
import { query, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { myTools } from './tools/index.js';
const SYSTEM_PROMPT = `You are a specialized agent that...
[Detailed instructions about capabilities and tools]`;
export async function runAgent(userPrompt: string) {
// Create MCP server at query time (not module level)
const server = createSdkMcpServer({
name: 'my-agent',
version: '1.0.0',
tools: myTools,
});
// Query with streaming
const messages = query({
prompt: userPrompt,
options: {
systemPrompt: SYSTEM_PROMPT,
permissionMode: 'bypassPermissions',
mcpServers: {
'my-agent': server,
},
},
});
// Process stream
for await (const event of messages) {
if (event.type === 'assistant') {
for (const content of event.message.content) {
if (content.type === 'text') {
process.stdout.write(content.text);
}
}
}
}
}
Pattern 2: Web Server Agent with SSE
Best for: Web applications, API servers, real-time UIs
// src/routes/agent.ts
import { query, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
app.post('/agent/stream', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const server = createSdkMcpServer({
name: 'web-agent',
version: '1.0.0',
tools: createContextualTools(req.session),
});
const messages = query({
prompt: req.body.prompt,
options: {
systemPrompt: SYSTEM_PROMPT,
mcpServers: { 'web-agent': server },
},
});
for await (const event of messages) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
res.end();
});
Pattern 3: Plugin/Framework Agent
Best for: Extensible systems, plugin architectures, reusable frameworks
// src/engine.ts
export class AgentEngine {
constructor(private plugin: Plugin) {}
async stream(prompt: string): Promise<void> {
const mcpServers: Record<string, any> = {};
if (this.plugin.tools.length > 0) {
const serverName = `${this.plugin.name}-tools`;
mcpServers[serverName] = createSdkMcpServer({
name: serverName,
version: this.plugin.version ?? '1.0.0',
tools: this.plugin.tools,
});
}
const messages = query({
prompt,
options: {
systemPrompt: this.plugin.systemPrompt,
permissionMode: 'bypassPermissions',
allowedTools: this.plugin.allowedTools,
mcpServers,
},
});
for await (const event of messages) {
await this.plugin.handleEvent(event);
}
}
}
Pattern 4: Monorepo Full-Stack Application
Best for: Full-stack apps with frontend + backend, complex web applications
Site Studio is an example of this pattern using npm workspaces.
Project Structure:
my-app/
├── package.json # Root workspace config
├── packages/
│ ├── backend/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts # Express server
│ │ │ ├── agent.ts # Agent logic
│ │ │ └── tools/ # Tool definitions
│ │ └── dist/ # Build output
│ └── frontend/
│ ├── package.json
│ ├── src/
│ └── dist/
Root package.json:
{
"name": "my-app",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
"dev:backend": "npm run dev --workspace=packages/backend",
"dev:frontend": "npm run dev --workspace=packages/frontend",
"build": "npm run build --workspace=packages/backend && npm run build --workspace=packages/frontend"
},
"devDependencies": {
"concurrently": "^9.1.2"
}
}
Backend package.json:
{
"name": "@my-app/backend",
"type": "module",
"scripts": {
"dev": "tsc && node dist/index.js",
"build": "tsc"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.14",
"express": "^4.21.2",
"zod": "^3.24.1"
}
}
Backend agent.ts:
import { query, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { createFileTools } from './tools/file-tools.js';
export async function runAgent(
prompt: string,
projectPath: string,
sessionId?: string
): Promise<AsyncIterable<any>> {
const tools = createFileTools(projectPath);
const server = createSdkMcpServer({
name: 'my-app',
version: '1.0.0',
tools,
});
const queryOptions: any = {
permissionMode: 'bypassPermissions', // Required for standalone servers!
systemPrompt: SYSTEM_PROMPT,
mcpServers: { 'my-app': server },
};
if (sessionId) {
queryOptions.resume = sessionId;
}
return query({ prompt, options: queryOptions });
}
Backend index.ts (Express):
import express from 'express';
import { runAgent } from './agent.js';
const app = express();
app.use(express.json());
app.post('/api/query', async (req, res) => {
const { prompt, projectId, sessionId } = req.body;
const projectPath = path.join(PROJECTS_DIR, projectId);
// Set up SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const stream = await runAgent(prompt, projectPath, sessionId);
for await (const event of stream) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error: any) {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
app.listen(3001, () => {
console.log('Backend running on http://localhost:3001');
});
Benefits of Monorepo Pattern:
- Share TypeScript configurations
- Single
npm installfor all dependencies - Coordinated development with
concurrently - Type safety across packages
- Unified build and deploy processes
Tool Wrapping Pattern (Preferred)
Philosophy: Create portable, composable tools as modules. Only wrap in MCP servers when needed for query execution.
Basic Tool Definition
// src/tools/my-tool.ts
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
export const myTool = tool(
'my_tool_name',
'Clear description of what this tool does',
z.object({
param1: z.string().describe('What param1 is for'),
param2: z.number().optional().describe('Optional parameter'),
}).shape,
async (params) => {
// Implementation
const result = await doSomething(params.param1, params.param2);
return {
content: [{
type: 'text',
text: JSON.stringify(result, null, 2),
}],
};
}
);
Tool Composition Pattern
// src/tools/index.ts
import { fileTools } from './file-tools.js';
import { apiTools } from './api-tools.js';
import { searchTool } from './search-tool.js';
// Export individual tools for selective use
export { fileTools, apiTools, searchTool };
// Export combined array for convenience
export const allTools = [
...fileTools,
...apiTools,
searchTool,
];
Tool Factories (Context-Aware Tools)
// src/tools/file-tools.ts
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
export function createFileTools(projectPath: string) {
const readFile = tool(
'read_file',
'Read a file from the project',
z.object({
path: z.string().describe('Relative path from project root'),
}).shape,
async ({ path }) => {
const fullPath = join(projectPath, path);
const content = await fs.readFile(fullPath, 'utf-8');
return {
content: [{ type: 'text', text: content }],
};
}
);
const writeFile = tool(
'write_file',
'Write content to a file',
z.object({
path: z.string().describe('Relative path from project root'),
content: z.string().describe('File content'),
}).shape,
async ({ path, content }) => {
const fullPath = join(projectPath, path);
await fs.writeFile(fullPath, content, 'utf-8');
return {
content: [{ type: 'text', text: `Wrote to ${path}` }],
};
}
);
return [readFile, writeFile];
}
// Usage in agent:
const tools = createFileTools('/path/to/project');
const server = createSdkMcpServer({ name: 'files', version: '1.0.0', tools });
Subprocess Wrapper Tools
// src/tools/python-tools.ts
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { spawn } from 'child_process';
import { z } from 'zod';
export const runPythonScript = tool(
'run_python_analysis',
'Run Python data analysis script',
z.object({
query: z.string().describe('Search query for analysis'),
limit: z.number().optional().describe('Max results'),
}).shape,
async ({ query, limit = 10 }) => {
return new Promise((resolve, reject) => {
const pythonProcess = spawn('python3', [
'scripts/analyze.py',
'--query', query,
'--limit', String(limit),
]);
let stdout = '';
let stderr = '';
pythonProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
pythonProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
pythonProcess.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Python script failed: ${stderr}`));
return;
}
try {
const result = JSON.parse(stdout);
resolve({
content: [{
type: 'text',
text: JSON.stringify(result, null, 2),
}],
});
} catch (e) {
reject(new Error(`Failed to parse output: ${stdout}`));
}
});
});
}
);
Dynamic Tool Creation
// src/tools/dynamic-tools.ts
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
export function createDynamicTools(config: ToolConfig[]) {
return config.map((toolConfig) => {
// Build Zod schema from config
const schemaShape: Record<string, z.ZodType> = {};
for (const param of toolConfig.parameters) {
let zodType: z.ZodType = z.string();
if (param.type === 'number') zodType = z.number();
if (param.type === 'boolean') zodType = z.boolean();
if (param.optional) zodType = zodType.optional();
zodType = zodType.describe(param.description);
schemaShape[param.name] = zodType;
}
return tool(
toolConfig.name,
toolConfig.description,
z.object(schemaShape).shape,
async (params) => {
// Execute configured command
const result = await executeCommand(toolConfig.command, params);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
}
);
});
}
Project Setup Checklist
1. Package Configuration
// package.json
{
"name": "my-agent",
"version": "1.0.0",
"type": "module",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.14",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.11.0",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}
}
2. TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3. Environment Setup
# .env.example (optional - for your own environment variables)
NODE_ENV=development
// .gitignore
node_modules/
dist/
.env
*.log
.DS_Store
4. Directory Structure
my-agent/
├── src/
│ ├── index.ts # Entry point
│ ├── agent.ts # Agent implementation
│ ├── tools/
│ │ ├── index.ts # Tool exports
│ │ ├── file-tools.ts # File operations
│ │ └── api-tools.ts # API calls
│ ├── prompts/
│ │ └── system.ts # System prompts
│ └── types/
│ └── index.ts # Type definitions
├── scripts/ # Python/shell scripts called by tools
├── package.json
├── tsconfig.json
├── .env
└── .env.example
System Prompts
Structure
System prompts should be 100-170+ lines with:
- Role and capabilities
- Detailed tool descriptions and when to use them
- Workflow instructions
- Output formatting requirements
- Error handling guidelines
// src/prompts/system.ts
export const SYSTEM_PROMPT = `You are a specialized agent with the following capabilities:
## Role
[Clear description of the agent's purpose and responsibilities]
## Available Tools
### tool_name_1
**Purpose:** [What it does]
**When to use:** [Specific scenarios]
**Parameters:**
- param1: [Description]
- param2: [Description]
**Example usage:** [Concrete example]
### tool_name_2
[Same structure...]
## Workflow
1. First, [initial step with tool usage]
2. Then, [subsequent steps]
3. Finally, [completion steps]
## Output Format
Always structure your responses as:
- [Format specification]
- [Examples]
## Error Handling
If a tool fails:
- [Recovery strategies]
- [Alternative approaches]
## Important Notes
- [Critical constraints]
- [Edge cases to handle]
`;
Streaming and Event Handling
Tool Execution Event Flow
When tools are executed, events follow this specific pattern that mirrors the Anthropic Messages API:
1. assistant event with tool_use block:
Claude requests to use a tool. The event contains:
{
type: 'assistant',
message: {
role: 'assistant',
content: [{
type: 'tool_use',
id: 'toolu_01AvhNWuX3aeWaaouT5t6D73', // Unique ID for this tool call
name: 'mcp__server__tool_name',
input: { param1: 'value' }
}]
}
}
2. user event with tool_result block:
The system/environment provides the tool result. Note: This is a user event, not an assistant event!
{
type: 'user',
message: {
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: 'toolu_01AvhNWuX3aeWaaouT5t6D73', // References the tool_use.id
content: [
{ type: 'text', text: '{"result": "data"}' }
],
is_error: false
}]
}
}
3. assistant event with text response:
Claude processes the result and responds to the user.
{
type: 'assistant',
message: {
role: 'assistant',
content: [{
type: 'text',
text: 'Based on the results...'
}]
}
}
Key Points:
- Tool results come in
userevents, notassistantevents - Match results using
tool_result.tool_use_id↔tool_use.id - Tool output is in the
contentarray (usuallycontent[0].text) - This structure mirrors how you'd interact with the Anthropic API directly
- From Claude's perspective: it requests a tool (
assistant), the world responds (user), then it continues (assistant)
Example: Tracking Tool Execution
const toolExecutions = new Map();
for await (const event of messages) {
if (event.type === 'assistant' && event.message?.content) {
for (const block of event.message.content) {
if (block.type === 'tool_use') {
// Store tool execution with its ID
toolExecutions.set(block.id, {
name: block.name,
input: block.input,
status: 'running'
});
}
}
}
if (event.type === 'user' && event.message?.content) {
for (const block of event.message.content) {
if (block.type === 'tool_result') {
// Match result with the tool call
const tool = toolExecutions.get(block.tool_use_id);
if (tool) {
tool.status = block.is_error ? 'error' : 'success';
tool.output = block.content.map(c => c.text).join('\n');
}
}
}
}
}
Pattern 1: Simple Text Streaming
for await (const event of messages) {
if (event.type === 'assistant') {
for (const content of event.message.content) {
if (content.type === 'text') {
process.stdout.write(content.text);
}
}
}
}
Pattern 2: Structured Event Processing
for await (const event of messages) {
switch (event.type) {
case 'assistant':
// Handle assistant messages (including tool_use blocks)
for (const block of event.message.content) {
if (block.type === 'text') {
await handleTextContent(block.text);
} else if (block.type === 'tool_use') {
await handleToolUse(block);
}
}
break;
case 'user':
// Handle tool results
for (const block of event.message.content) {
if (block.type === 'tool_result') {
await handleToolResult(block);
}
}
break;
case 'error':
await handleError(event.error);
break;
}
}
Pattern 3: Web SSE Streaming
for await (const event of messages) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
if (event.type === 'assistant') {
// Optionally send custom events
res.write(`event: message\ndata: ${JSON.stringify({
role: 'assistant',
content: event.message.content
})}\n\n`);
}
}
Pattern 4: Web UI with Separate Text Sections (CRITICAL)
Problem: When building web UIs that display conversation with interleaved text and tools, a common mistake is accumulating ALL text into one string. This causes text from different conversation sections to merge incorrectly.
❌ WRONG - Text accumulates forever:
let fullTextResponse = ''; // DON'T DO THIS
for await (const event of messages) {
if (event.type === 'assistant' && event.message?.content) {
for (const block of event.message.content) {
if (block.type === 'text') {
fullTextResponse += block.text; // ❌ Never resets!
// Display fullTextResponse
}
}
}
}
This creates: "Text1 Text2 Text3" all merged together, even when tools appear between them.
✅ CORRECT - Section-based text management:
let currentSectionText = ''; // Resets after each tool group
let contentBlocks = []; // Structured: [text, tools, text, tools, text]
let processedToolIds = new Set();
for await (const event of messages) {
if (event.type === 'assistant' && event.message?.content) {
for (const block of event.message.content) {
if (block.type === 'text') {
// Accumulate text for CURRENT section only
currentSectionText += block.text;
// Update or create text block for this section
let textBlockIndex = contentBlocks.findIndex((b, i) => {
if (b.type === 'text') {
// Find last text block (no tools after it)
const hasToolsAfter = contentBlocks.slice(i + 1).some(cb => cb.type === 'tools');
return !hasToolsAfter;
}
return false;
});
if (textBlockIndex >= 0) {
contentBlocks[textBlockIndex].text = currentSectionText;
} else {
contentBlocks.push({ type: 'text', text: currentSectionText });
}
} else if (block.type === 'tool_use') {
// Only process if not duplicate
if (!processedToolIds.has(block.id)) {
processedToolIds.add(block.id);
// Finalize current text section
if (currentSectionText.trim()) {
const lastBlock = contentBlocks[contentBlocks.length - 1];
if (lastBlock?.type === 'text') {
lastBlock.text = currentSectionText;
}
}
// RESET for next section - this is the key!
currentSectionText = '';
// Add tool to tools block
// (implementation depends on your UI structure)
}
}
}
}
}
Result: Properly separated structure:
contentBlocks = [
{ type: 'text', text: 'I will scaffold your site...' },
{ type: 'tools', tools: [scaffoldTool] },
{ type: 'text', text: 'Now I will read the files...' },
{ type: 'tools', tools: [readTool1, readTool2] },
{ type: 'text', text: 'I have updated your files...' }
]
Key Principles:
- Reset
currentSectionTextafter tools - Don't accumulate across tool boundaries - Deduplicate tools - Use a Set to track processed tool IDs
- Separate text and tool blocks - Create structured sections, not one giant text blob
- Update the current text block - Don't create a new text block on every streaming chunk
Reference: See /home/zweb/apps/obsidian-agent-plugin/src/main.ts lines 915-1010 for a working implementation of this pattern.
Error Handling
Tool Error Handling
export const myTool = tool(
'my_tool',
'Description',
schema,
async (params) => {
try {
const result = await riskyOperation(params);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: true,
message: error.message,
suggestion: 'Try adjusting parameters or check prerequisites',
}),
}],
isError: true,
};
}
}
);
Stream Error Handling
try {
for await (const event of messages) {
// Process events
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Stream aborted by user');
} else if (error.status === 429) {
console.error('Rate limit exceeded. Please wait and retry.');
} else {
console.error('Agent error:', error.message);
}
}
Best Practices
DO
✅ Export tools as both individual exports and arrays
export const tool1 = tool(...);
export const tool2 = tool(...);
export const myTools = [tool1, tool2];
✅ Use Zod schemas with detailed descriptions
z.object({
query: z.string().describe('The search query to execute'),
limit: z.number().optional().describe('Maximum number of results (default: 10)'),
})
✅ Create MCP servers at query time, not module level
// GOOD: In function scope
async function runAgent() {
const server = createSdkMcpServer({ tools });
const messages = query({ options: { mcpServers: { 'name': server } } });
}
✅ Use closures for context-aware tools
export function createTools(context: Context) {
return [
tool('name', 'desc', schema, async (params) => {
// Has access to context via closure
return processWithContext(context, params);
})
];
}
✅ Include detailed system prompts (100+ lines)
✅ Set type: "module" in package.json
✅ Authentication handled automatically by Claude Code
DON'T
❌ Don't create full MCP servers for simple tools
// AVOID: Creating MCP server infrastructure
const server = new MCPServer(...);
server.registerTool(...);
// Use tool() wrapper instead
❌ Don't use global state in tools
// BAD
let globalContext;
export const tool = tool('name', 'desc', schema, async () => {
return useGlobalContext(globalContext); // Brittle!
});
❌ Don't mix module types
// package.json should have "type": "module"
// Don't mix require() and import
❌ Don't skip Zod validation
// BAD: No schema validation
tool('name', 'desc', {}, async (params: any) => { ... });
Permission Modes
CRITICAL: Standalone servers must use bypassPermissions
When building standalone applications (Express servers, CLI tools), always use 'bypassPermissions':
// ✅ CORRECT for standalone servers
const messages = query({
prompt,
options: {
permissionMode: 'bypassPermissions', // Always use this!
systemPrompt,
mcpServers,
},
});
Why? The 'interactive' mode requires Claude Code to be running to show permission prompts. Standalone servers run independently and cannot display interactive prompts, leading to "Claude Code process exited with code 1" errors.
// ❌ WRONG for standalone servers
permissionMode: mode === 'execute' ? 'bypassPermissions' : 'interactive',
// This will crash when mode is 'plan'!
When to use each mode:
'bypassPermissions'- Standalone servers, CLI tools, background agents'interactive'- Only when running inside Claude Code with UI for approvals'allowedTools'- When you want to restrict tool access programmatically
Common Patterns
CSV Processing Agent
import { tool } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
import { parse } from 'csv-parse/sync';
import { stringify } from 'csv-stringify/sync';
export const readCsv = tool(
'read_csv',
'Read and parse CSV file',
z.object({
path: z.string(),
}).shape,
async ({ path }) => {
const content = await fs.readFile(path, 'utf-8');
const records = parse(content, { columns: true });
return {
content: [{
type: 'text',
text: JSON.stringify(records, null, 2),
}],
};
}
);
export const writeCsv = tool(
'write_csv',
'Write data to CSV file',
z.object({
path: z.string(),
data: z.array(z.record(z.any())),
}).shape,
async ({ path, data }) => {
const csv = stringify(data, { header: true });
await fs.writeFile(path, csv, 'utf-8');
return {
content: [{
type: 'text',
text: `Wrote ${data.length} records to ${path}`,
}],
};
}
);
API Integration Tools
export const searchApi = tool(
'search_api',
'Search external API',
z.object({
query: z.string(),
filters: z.record(z.string()).optional(),
}).shape,
async ({ query, filters }) => {
const params = new URLSearchParams({ q: query, ...filters });
const response = await fetch(`https://api.example.com/search?${params}`);
if (!response.ok) {
return {
content: [{
type: 'text',
text: JSON.stringify({ error: `API error: ${response.statusText}` }),
}],
isError: true,
};
}
const data = await response.json();
return {
content: [{
type: 'text',
text: JSON.stringify(data, null, 2),
}],
};
}
);
Debugging Tips
1. Enable Verbose Logging
const messages = query({
prompt,
options: {
systemPrompt,
mcpServers,
// Add debugging
onToolCall: (toolName, params) => {
console.log(`[TOOL CALL] ${toolName}`, params);
},
onToolResult: (toolName, result) => {
console.log(`[TOOL RESULT] ${toolName}`, result);
},
},
});
2. Validate Tool Definitions
// Add validation helper
function validateTools(tools: any[]) {
for (const t of tools) {
if (!t.name) console.error('Tool missing name:', t);
if (!t.description) console.error('Tool missing description:', t);
if (!t.inputSchema) console.error('Tool missing schema:', t);
}
}
3. Test Tools Independently
// Test tools before using in agent
async function testTool() {
const result = await myTool.execute({ param1: 'test' });
console.log('Tool result:', result);
}
Resources
This skill references the following bundled resources:
references/
api-reference.md- Official SDK API documentationworking-examples.md- Complete examples from production implementationstroubleshooting.md- Common issues and solutions
assets/
project-template/- Starter template for new agent projectstsconfig-template.json- Recommended TypeScript configuration
Didn't find tool you were looking for?