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.
@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.).
npm install @langchain/core @langchain/openai zod langchain
# or for Claude:
# npm install @langchain/anthropic
// 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,
];
// 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);
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);
# .env
JETTYD_API_KEY=tk_your_key_here
OPENAI_API_KEY=sk-... # for OpenAI
# ANTHROPIC_API_KEY=... # for Claude