LangChain — Tool Wrapper

Wrap jettyd REST endpoints as DynamicStructuredTool objects and use them with any LangChain agent. Gives any LangChain-compatible LLM the ability to read sensors and send commands.

Prerequisites Node.js 18+, a jettyd API key, and @langchain/core v0.3+. The MCP server is the easier integration if you use Claude — this approach works with any LLM (GPT-4, Gemini, Llama, etc.).

Install dependencies

npm install @langchain/core @langchain/openai zod langchain
# or for Claude:
# npm install @langchain/anthropic

Tool definitions

// src/jettyd-tools.ts
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

const API_BASE = 'https://api.jettyd.com/v1';
const API_KEY  = process.env['JETTYD_API_KEY'] ?? '';

async function jettydFetch(
  method: string,
  path: string,
  body?: unknown,
): Promise<unknown> {
  const res = await fetch(`${API_BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    ...(body ? { body: JSON.stringify(body) } : {}),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`jettyd API error ${res.status}: ${text}`);
  }
  if (res.status === 204) return null;
  return res.json();
}

// ── Tool: list_devices ────────────────────────────────────────────────────────

export const listDevicesTool = new DynamicStructuredTool({
  name: 'list_devices',
  description:
    'List all IoT devices in the jettyd tenant with their current online/offline status.',
  schema: z.object({
    status: z
      .enum(['online', 'offline', 'all'])
      .optional()
      .describe('Filter by device status'),
  }),
  func: async ({ status }) => {
    const params = status && status !== 'all' ? `?status=${status}` : '';
    const data = await jettydFetch('GET', `/devices${params}`);
    return JSON.stringify(data, null, 2);
  },
});

// ── Tool: read_sensor ─────────────────────────────────────────────────────────

export const readSensorTool = new DynamicStructuredTool({
  name: 'read_sensor',
  description: 'Read the latest sensor value from a jettyd IoT device.',
  schema: z.object({
    device_id: z.string().describe('Device UUID'),
    metric: z
      .string()
      .describe('Sensor metric name, e.g. soil.moisture, temperature, humidity'),
  }),
  func: async ({ device_id, metric }) => {
    const data = (await jettydFetch(
      'GET',
      `/devices/${device_id}/telemetry?metric=${encodeURIComponent(metric)}&limit=1`,
    )) as Array<unknown>;
    return JSON.stringify(data?.[0] ?? null, null, 2);
  },
});

// ── Tool: get_history ─────────────────────────────────────────────────────────

export const getHistoryTool = new DynamicStructuredTool({
  name: 'get_history',
  description: 'Get historical telemetry data for a device metric.',
  schema: z.object({
    device_id: z.string().describe('Device UUID'),
    metric: z.string().describe('Sensor metric name'),
    period: z
      .enum(['1h', '6h', '24h', '7d', '30d'])
      .default('24h')
      .describe('Time window'),
  }),
  func: async ({ device_id, metric, period }) => {
    const data = await jettydFetch(
      'GET',
      `/devices/${device_id}/telemetry?metric=${encodeURIComponent(metric)}&period=${period}`,
    );
    return JSON.stringify(data, null, 2);
  },
});

// ── Tool: send_command ────────────────────────────────────────────────────────

export const sendCommandTool = new DynamicStructuredTool({
  name: 'send_command',
  description:
    'Send a command to a jettyd IoT device, e.g. valve.on, relay.off, led.set.',
  schema: z.object({
    device_id: z.string().describe('Device UUID'),
    action: z.string().describe('Command name, e.g. valve.on'),
    params: z
      .record(z.unknown())
      .optional()
      .describe('Command parameters as key-value pairs'),
  }),
  func: async ({ device_id, action, params }) => {
    await jettydFetch('POST', `/devices/${device_id}/commands`, {
      command_type: action,
      payload: params ?? {},
    });
    return JSON.stringify({ success: true, action, device_id });
  },
});

// ── Tool: get_fleet_status ────────────────────────────────────────────────────

export const getFleetStatusTool = new DynamicStructuredTool({
  name: 'get_fleet_status',
  description:
    'Get an overview of all jettyd devices — total, online, and offline counts.',
  schema: z.object({}),
  func: async () => {
    const devices = (await jettydFetch('GET', '/devices')) as Array<{
      status: string;
    }>;
    const online  = devices.filter((d) => d.status === 'online').length;
    const offline = devices.filter((d) => d.status === 'offline').length;
    return JSON.stringify({ total: devices.length, online, offline });
  },
});

export const jettydTools = [
  listDevicesTool,
  readSensorTool,
  getHistoryTool,
  sendCommandTool,
  getFleetStatusTool,
];

Use with an OpenAI agent

// src/agent.ts
import { ChatOpenAI } from '@langchain/openai';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { jettydTools } from './jettyd-tools.js';

const llm = new ChatOpenAI({
  model: 'gpt-4o',
  temperature: 0,
});

const prompt = ChatPromptTemplate.fromMessages([
  ['system', 'You are an IoT assistant with access to jettyd IoT devices. Help the user monitor and control their devices.'],
  ['human', '{input}'],
  ['placeholder', '{agent_scratchpad}'],
]);

const agent = createToolCallingAgent({ llm, tools: jettydTools, prompt });
const executor = new AgentExecutor({ agent, tools: jettydTools });

const result = await executor.invoke({
  input: 'What is the current soil moisture on my greenhouse device? If it is below 30%, turn on the irrigation valve.',
});

console.log(result.output);

Use with Claude (Anthropic)

import { ChatAnthropic } from '@langchain/anthropic';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { jettydTools } from './jettyd-tools.js';

const llm = new ChatAnthropic({
  model: 'claude-sonnet-4-6',
  temperature: 0,
});

const prompt = ChatPromptTemplate.fromMessages([
  ['system', 'You are an IoT assistant with access to jettyd IoT devices.'],
  ['human', '{input}'],
  ['placeholder', '{agent_scratchpad}'],
]);

const agent = createToolCallingAgent({ llm, tools: jettydTools, prompt });
const executor = new AgentExecutor({ agent, tools: jettydTools });

const result = await executor.invoke({
  input: 'Show me the fleet status and then read the temperature from all online devices.',
});
console.log(result.output);

Environment setup

# .env
JETTYD_API_KEY=tk_your_key_here
OPENAI_API_KEY=sk-...        # for OpenAI
# ANTHROPIC_API_KEY=...      # for Claude
Tip: use the MCP server for Claude If you're building with Claude Desktop or any MCP-compatible client, the jettyd MCP server provides all 9 tools with OAuth PKCE — no API key management required. Use this LangChain approach for custom agents, non-Claude LLMs, or when you need programmatic control over tool selection.

Back to Examples