Model Context Protocol (MCP) integration, tool registration, and external service connectivity in the HAIP SDK
// Register available tools
await client.listTools("AGENT", [
{ name: "search", description: "Search the web for information" },
{ name: "calculator", description: "Perform mathematical calculations" },
{ name: "weather", description: "Get weather information for a location" },
]);
// Handle tool calls
client.on("message", (message: HAIPMessage) => {
if (message.type === "TOOL_CALL") {
handleToolCall(message);
}
});
// Define tools
const tools = [
{
name: "search",
description: "Search the web for information",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
{
name: "calculator",
description: "Perform mathematical calculations",
parameters: {
type: "object",
properties: {
expression: {
type: "string",
description: "Mathematical expression to evaluate",
},
},
required: ["expression"],
},
},
];
// Register tools
await client.listTools("AGENT", tools);
async function handleToolCall(message: HAIPMessage) {
const { tool, params, call_id } = message.payload;
try {
// Update tool status to running
await client.updateTool("AGENT", call_id, "RUNNING", 0);
// Execute the tool
const result = await executeTool(tool, params);
// Complete the tool call
await client.completeTool("AGENT", call_id, "OK", result);
} catch (error) {
console.error(`Tool execution failed:`, error);
await client.completeTool("AGENT", call_id, "ERROR", {
error: error.message,
});
}
}
async function executeTool(tool: string, params: any) {
switch (tool) {
case "search":
return await performSearch(params.query);
case "calculator":
return { result: eval(params.expression) };
case "weather":
return await getWeather(params.location);
default:
throw new Error(`Unknown tool: ${tool}`);
}
}
// Tool is queued
await client.updateTool("AGENT", callId, "QUEUED");
// Tool is running with progress
await client.updateTool("AGENT", callId, "RUNNING", 50); // 50% progress
// Tool is running with partial results
await client.updateTool("AGENT", callId, "RUNNING", 75, {
partial: "Partial results available...",
});
// Tool is being cancelled
await client.updateTool("AGENT", callId, "CANCELLING");
// Successful completion
await client.completeTool("AGENT", callId, "OK", {
results: ["Result 1", "Result 2"],
metadata: { source: "web-search" },
});
// Error completion
await client.completeTool("AGENT", callId, "ERROR", {
error: "Network timeout",
details: "Failed to connect to search service",
});
// Cancelled completion
await client.completeTool("AGENT", callId, "CANCELLED", {
reason: "User cancelled the operation",
});
class WebSearchTool {
constructor(private apiKey: string) {}
async search(query: string): Promise<any> {
const response = await fetch(
`https://api.search.com/search?q=${encodeURIComponent(query)}&key=${
this.apiKey
}`
);
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
const data = await response.json();
return {
results: data.results.map((result: any) => ({
title: result.title,
url: result.url,
snippet: result.snippet,
})),
totalResults: data.totalResults,
};
}
}
// Usage in tool handler
const searchTool = new WebSearchTool(process.env.SEARCH_API_KEY);
async function handleSearchTool(params: any) {
const results = await searchTool.search(params.query);
return {
query: params.query,
results: results.results,
totalResults: results.totalResults,
};
}
class CalculatorTool {
private safeEval(expression: string): number {
// Remove potentially dangerous characters
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, "");
try {
// Use Function constructor for safer evaluation
const result = new Function(`return ${sanitized}`)();
if (typeof result !== "number" || !isFinite(result)) {
throw new Error("Invalid calculation result");
}
return result;
} catch (error) {
throw new Error(`Calculation failed: ${error.message}`);
}
}
calculate(expression: string): any {
const result = this.safeEval(expression);
return {
expression,
result,
formatted: result.toLocaleString(),
};
}
}
// Usage
const calculator = new CalculatorTool();
async function handleCalculatorTool(params: any) {
return calculator.calculate(params.expression);
}
class WeatherTool {
constructor(private apiKey: string) {}
async getWeather(location: string): Promise<any> {
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${
this.apiKey
}&q=${encodeURIComponent(location)}`
);
if (!response.ok) {
throw new Error(`Weather API failed: ${response.statusText}`);
}
const data = await response.json();
return {
location: data.location.name,
temperature: data.current.temp_c,
condition: data.current.condition.text,
humidity: data.current.humidity,
windSpeed: data.current.wind_kph,
};
}
}
// Usage
const weatherTool = new WeatherTool(process.env.WEATHER_API_KEY);
async function handleWeatherTool(params: any) {
return await weatherTool.getWeather(params.location);
}
class ToolRegistry {
private tools = new Map<string, (params: any) => Promise<any>>();
register(name: string, handler: (params: any) => Promise<any>) {
this.tools.set(name, handler);
}
async execute(name: string, params: any): Promise<any> {
const handler = this.tools.get(name);
if (!handler) {
throw new Error(`Tool not found: ${name}`);
}
return await handler(params);
}
getToolNames(): string[] {
return Array.from(this.tools.keys());
}
}
// Usage
const registry = new ToolRegistry();
registry.register("search", handleSearchTool);
registry.register("calculator", handleCalculatorTool);
registry.register("weather", handleWeatherTool);
// Execute tool
const result = await registry.execute("search", { query: "TypeScript" });
async function getToolSchema(toolName: string) {
await client.getToolSchema("AGENT", toolName);
}
// Handle schema response
client.on("message", (message: HAIPMessage) => {
if (message.type === "TOOL_SCHEMA") {
const { tool, schema } = message.payload;
console.log(`Schema for ${tool}:`, schema);
}
});
class ToolErrorHandler {
static async handleToolExecution(
client: any,
callId: string,
toolName: string,
params: any,
executor: (params: any) => Promise<any>
) {
try {
// Update status to running
await client.updateTool("AGENT", callId, "RUNNING", 0);
// Execute with timeout
const result = await Promise.race([
executor(params),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Tool execution timeout")), 30000)
),
]);
// Complete successfully
await client.completeTool("AGENT", callId, "OK", result);
} catch (error) {
console.error(`Tool ${toolName} failed:`, error);
// Determine error type
let errorType = "ERROR";
let errorDetails = { error: error.message };
if (error.message.includes("timeout")) {
errorType = "ERROR";
errorDetails = { error: "Tool execution timed out" };
} else if (error.message.includes("permission")) {
errorType = "ERROR";
errorDetails = { error: "Insufficient permissions for this tool" };
} else if (error.message.includes("network")) {
errorType = "ERROR";
errorDetails = { error: "Network error occurred" };
}
await client.completeTool("AGENT", callId, errorType, errorDetails);
}
}
}
// Usage
await ToolErrorHandler.handleToolExecution(
client,
callId,
"search",
{ query: "TypeScript" },
handleSearchTool
);
class ToolRetryHandler {
static async executeWithRetry(
executor: () => Promise<any>,
maxRetries: number = 3,
delay: number = 1000
): Promise<any> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await executor();
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
console.log(
`Tool attempt ${attempt} failed, retrying in ${delay}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
throw lastError!;
}
}
// Usage in tool handler
async function handleSearchToolWithRetry(params: any) {
return await ToolRetryHandler.executeWithRetry(
() => searchTool.search(params.query),
3,
1000
);
}
class MockToolRegistry {
private mockResults = new Map<string, any>();
private mockErrors = new Map<string, Error>();
setMockResult(toolName: string, result: any) {
this.mockResults.set(toolName, result);
}
setMockError(toolName: string, error: Error) {
this.mockErrors.set(toolName, error);
}
async execute(toolName: string, params: any): Promise<any> {
const error = this.mockErrors.get(toolName);
if (error) {
throw error;
}
const result = this.mockResults.get(toolName);
if (result) {
return result;
}
throw new Error(`No mock result set for tool: ${toolName}`);
}
}
// Usage in tests
const mockRegistry = new MockToolRegistry();
mockRegistry.setMockResult("search", {
results: [{ title: "Test Result", url: "https://example.com" }],
totalResults: 1,
});
mockRegistry.setMockError("calculator", new Error("Invalid expression"));
// Test tool execution
const result = await mockRegistry.execute("search", { query: "test" });
expect(result.results).toHaveLength(1);
describe("Tool Integration", () => {
test("should handle tool call successfully", async () => {
const client = createHAIPClient({
url: "ws://localhost:8080",
token: "test-token",
transport: "websocket",
});
// Mock tool execution
const mockToolCall = {
type: "TOOL_CALL",
payload: {
tool: "calculator",
params: { expression: "2 + 2" },
call_id: "call-123",
},
};
// Set up expectations
const updateSpy = jest.spyOn(client, "updateTool");
const completeSpy = jest.spyOn(client, "completeTool");
// Simulate tool call
client.emit("message", mockToolCall);
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 100));
// Verify tool lifecycle
expect(updateSpy).toHaveBeenCalledWith("AGENT", "call-123", "RUNNING", 0);
expect(completeSpy).toHaveBeenCalledWith("AGENT", "call-123", "OK", {
result: 4,
});
});
});
class ToolMetrics {
private metrics = new Map<
string,
{
calls: number;
totalTime: number;
errors: number;
lastCall: number;
}
>();
recordCall(toolName: string, duration: number, success: boolean) {
const current = this.metrics.get(toolName) || {
calls: 0,
totalTime: 0,
errors: 0,
lastCall: 0,
};
current.calls++;
current.totalTime += duration;
current.lastCall = Date.now();
if (!success) {
current.errors++;
}
this.metrics.set(toolName, current);
}
getMetrics(toolName?: string) {
if (toolName) {
return this.metrics.get(toolName);
}
return Object.fromEntries(this.metrics);
}
getAverageTime(toolName: string): number {
const metrics = this.metrics.get(toolName);
if (!metrics || metrics.calls === 0) {
return 0;
}
return metrics.totalTime / metrics.calls;
}
getErrorRate(toolName: string): number {
const metrics = this.metrics.get(toolName);
if (!metrics || metrics.calls === 0) {
return 0;
}
return metrics.errors / metrics.calls;
}
}
// Usage
const toolMetrics = new ToolMetrics();
async function executeToolWithMetrics(
toolName: string,
executor: () => Promise<any>
): Promise<any> {
const startTime = Date.now();
let success = true;
try {
const result = await executor();
return result;
} catch (error) {
success = false;
throw error;
} finally {
const duration = Date.now() - startTime;
toolMetrics.recordCall(toolName, duration, success);
}
}
Was this page helpful?