Chat with Devices API
Note: This tutorial shows how to build an MCP (Model Context Protocol) server that lets you talk to the Senza Devices API using natural language through Claude. We'll recreate the functionality from our Device Lookup tutorial but instead of writing scripts, we'll just... talk.
The Conversation
User: Hey, I've heard you can talk to data sources like APIs. What is MCP, and how does it work?
Claude: Great question! MCP stands for Model Context Protocol. Think of it as a way for me (Claude) to connect to external tools and data sources.
Here's the basic idea:
- You write a small server (in Node.js, Python, or any language) that exposes certain "tools"
- Each tool is like a function I can call - for example, "get device info" or "update device description"
- When you ask me something that needs those tools, I automatically call them and use the results in my response
The cool part? Once it's set up, you don't write scripts or deal with API details anymore. You just ask me in plain English, and I handle the technical stuff behind the scenes.
For example, instead of running:
node device-lookup.js input.json output.json
You could just say: "Show me all my devices and update the ones that are missing descriptions."
Want to see it in action?
User: That's great. I saw that Senza has a Device Lookup tutorial that shows how to write a script that talks to the Senza Devices API. Could you make an MCP tool that does the same thing?
Claude: Absolutely! Let me create an MCP server that gives us those same capabilities. I'll build it so you can:
- List all devices in your tenant
- Get detailed info about specific devices
- Update device properties like community (Alpha/Beta/Stable) and descriptions
First, let's set up the project structure:
mkdir senza-devices-mcp-server
cd senza-devices-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk dotenv zod
npm install -D typescript tsx @types/node
Now I'll create the TypeScript configuration and source files. The key pieces are:
1. OAuth Authentication (src/services/authManager.ts)
This handles getting access tokens from the Senza auth server using your API credentials:
import { SENZA_AUTH_URL, SENZA_AUDIENCE } from "../constants.js";
export class AuthManager {
private credentials: OAuthCredentials;
private accessToken: string | null = null;
private tokenExpiryTime: number | null = null;
async getAccessToken(): Promise<string> {
// Check if we have a valid token
if (this.accessToken && Date.now() < this.tokenExpiryTime - 5 * 60 * 1000) {
return this.accessToken;
}
// Fetch a new token using OAuth client credentials flow
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.credentials.clientId,
client_secret: this.credentials.clientSecret,
audience: SENZA_AUDIENCE
});
const response = await fetch(SENZA_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
const data = await response.json();
this.accessToken = data.access_token;
this.tokenExpiryTime = Date.now() + (data.expires_in * 1000);
return this.accessToken;
}
}
2. API Client (src/services/senzaClient.ts)
This makes authenticated calls to the Devices API:
export class SenzaApiClient {
constructor(
private authManager: AuthManager,
private tenantId: string
) {}
async listDevices(): Promise<ListDevicesResponse> {
const token = await this.authManager.getAccessToken();
const response = await fetch(
`${SENZA_API_BASE_URL}/tenants/1.0/${this.tenantId}/devices`,
{ headers: { Authorization: `Bearer ${token}` } }
);
return response.json();
}
async getDevice(deviceId: string): Promise<Device> {
const token = await this.authManager.getAccessToken();
const response = await fetch(
`${SENZA_API_BASE_URL}/devices/1.0/${deviceId}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const data = await response.json();
return data.device ?? data;
}
async updateDevice(deviceId: string, updates: DeviceUpdatePayload): Promise<Device> {
const token = await this.authManager.getAccessToken();
const response = await fetch(
`${SENZA_API_BASE_URL}/devices/1.0/${deviceId}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
}
);
const data = await response.json();
return data.device ?? data;
}
}
3. Tool Registration (src/tools/deviceTools.ts)
This is where we define the tools I can call:
export function registerDeviceTools(server: McpServer, client: SenzaApiClient) {
// List devices tool
server.registerTool("senza_list_devices", {
description: "List all devices in the tenant",
inputSchema: ListDevicesInputSchema
}, async (params) => {
const response = await client.listDevices();
return {
content: [{
type: "text",
text: formatDeviceListMarkdown(response.devices)
}]
};
});
// Get device tool
server.registerTool("senza_get_device", {
description: "Get detailed device information by device ID",
inputSchema: GetDeviceInputSchema
}, async (params) => {
const device = await client.getDevice(params.deviceId);
return {
content: [{
type: "text",
text: formatDeviceMarkdown(device)
}]
};
});
// Update device tool
server.registerTool("senza_update_device", {
description: "Update device community or description",
inputSchema: UpdateDeviceInputSchema
}, async (params) => {
const { deviceId, response_format, ...updates } = params;
const device = await client.updateDevice(deviceId, updates);
return {
content: [{
type: "text",
text: formatDeviceMarkdown(device)
}]
};
});
}
4. Main Entry Point (src/index.ts)
This ties everything together and starts the MCP server:
import "dotenv/config";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { AuthManager } from "./services/authManager.js";
import { SenzaApiClient } from "./services/senzaClient.js";
import { registerDeviceTools } from "./tools/deviceTools.js";
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const TENANT_ID = process.env.TENANT_ID;
if (!CLIENT_ID || !CLIENT_SECRET || !TENANT_ID) {
console.error("Error: CLIENT_ID, CLIENT_SECRET, and TENANT_ID required");
process.exit(1);
}
const server = new McpServer({
name: "senza-devices-mcp-server",
version: "1.0.0"
});
const authManager = new AuthManager({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
tenantId: TENANT_ID
});
const senzaClient = new SenzaApiClient(authManager, TENANT_ID);
registerDeviceTools(server, senzaClient);
async function runStdio() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Senza Devices MCP server running");
}
runStdio().catch(console.error);
Now let's create a .env file with your API credentials:
CLIENT_ID=your_client_id_here
CLIENT_SECRET=your_client_secret_here
TENANT_ID=your_tenant_id_here
Getting API Credentials: Go to the Senza Console → API Keys → Create Application. Select your tenant and enable
senza:device:readandsenza:device:writepermissions.
Build and test it:
npm run build
npm start
The full source code for this MCP server is available at: https://github.com/synamedia-senza/senza-devices-mcp-server
User: That's awesome. How do I configure Claude to use it?
Claude: Easy! You need to tell Claude Desktop about your MCP server. Here's how:
1. Find your Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
2. Add this configuration:
{
"mcpServers": {
"senza-devices": {
"command": "node",
"args": [
"/absolute/path/to/senza-devices-mcp-server/dist/index.js"
],
"env": {
"CLIENT_ID": "your_client_id",
"CLIENT_SECRET": "your_client_secret",
"TENANT_ID": "your_tenant_id"
}
}
}
}
Important: Replace /absolute/path/to/ with the actual full path to your project directory!
3. Restart Claude Desktop
Quit Claude Desktop completely (not just closing the window) and reopen it. That's it! Now I'll have access to your Senza devices.
User: Okay, I've restarted. Can you list the devices?
Claude: [Calls the senza_list_devices tool]
Here are all the devices in your tenant:
Devices in Tenant (50 total)
| Device ID | Status | Community | Description |
|---|---|---|---|
| 7e6e70960d21af04 | Activated | Stable | - |
| 7e6e767a0d21af04 | Activated | Stable | HackerDojo #1 |
| 7e6e8eb10d21af04 | Activated | Stable | Elias |
| 7e6e07f20d21af04 | Activated | Stable | - |
| 7e6e76780d21af04 | Activated | Stable_special_ui | Hacker Dojo Stage |
| 7e6e76350d21af04 | Activated | Stable | Ali |
| 7e6eba5a0d21af04 | Activated | Stable | Andrew M |
| 7e6e787a0d21af04 | Activated | Stable_special_ui | James |
| 7e6eb88c0d21af04 | Activated | Stable | Tino |
| ... (41 more devices)
You have 50 devices total:
- 13 are Activated (actively connected)
- 37 are Registered (registered but not recently active)
Most devices are on the Stable community channel.
User: Amazing! I can't believe how easy this is. When did Elias last use his device?
Claude: [Calls senza_get_device with deviceId "7e6e8eb10d21af04"]
Device: 7e6e8eb10d21af04
Status: Activated
Tenant: cku6kacd
Community: Stable
Description: Elias
Activated: May 23, 2025
Last Connection: July 22, 2025 at 7:07 PM
Last IP: 73.71.170.249
Elias last connected about 6 months ago on July 22, 2025 at 7:07 PM. His device has been activated since May and is currently on the Stable release channel.
User: Got it. Can you move his device to the beta community?
Claude: [Calls senza_update_device to change community to Beta]
Done! Elias's device is now on the Beta community. Let me verify:
[Calls senza_get_device to confirm]
Device: 7e6e8eb10d21af04
Status: Activated
Tenant: cku6kacd
Community: Beta ✓
Description: Elias
Activated: May 23, 2025
Last Connection: July 22, 2025 at 7:07 PM
Last IP: 73.71.170.249
Perfect! The device is now on Beta - it'll receive beta releases going forward.
User: Perfect. I just gave device 7e6e07f20d21af04 to Sam. Could you update the description?
Claude: [Calls senza_update_device to set description to "Sam"]
Updated! Let me confirm:
[Calls senza_get_device to verify]
Device: 7e6e07f20d21af04
Status: Activated
Tenant: cku6kacd
Community: Stable
Description: Sam ✓
Activated: May 17, 2025
Last Connection: July 2, 2025
Last IP: 12.38.208.106
Great! The device description is now set to "Sam". The device is currently on Stable and last connected in early July.
User: Thanks. This is awesome!
Claude: You're welcome! Pretty cool, right?
Here's what we accomplished:
- Built an MCP server that talks to the Senza Devices API
- Used OAuth authentication (no manual token management!)
- Created three tools: list devices, get device details, and update devices
- Now you can manage devices just by asking me in plain English
The best part? This same pattern works for any API:
- Want to check analytics? Build an MCP server for your BigQuery or MongoDB
- Need to manage content? Connect to your CMS API
- Want to control IoT devices? MCP can do that too
Instead of writing scripts and remembering API endpoints, you just... talk. And I handle the technical details.
User: Could you save a workflow document that explains how all this works?
Claude: Absolutely! Let me create a workflow document that explains the architecture and how to use these tools effectively.
[Creates /Users/yourname/Projects/senza-devices-workflow.md]
Here's what I've documented:
Senza Devices MCP Workflow
Overview
The Senza Devices MCP Server provides natural language access to device management through Claude.
Architecture
┌─────────────────┐
│ Claude Desktop │
│ │
│ "List devices" │
└────────┬────────┘
│
↓
┌────────────────────┐
│ MCP Server │
│ (Node.js/stdio) │
│ │
│ • listDevices() │
│ • getDevice() │
│ • updateDevice() │
└────────┬───────────┘
│
↓ OAuth Token
┌────────────────────┐
│ Senza API │
│ │
│ • GET /tenants/ │
│ • GET /devices/ │
│ • PUT /devices/ │
└────────────────────┘
Authentication Flow
- Initial Setup: Store CLIENT_ID, CLIENT_SECRET, TENANT_ID in config
- Token Request: MCP server requests access token from auth server
- Token Caching: Token cached and automatically refreshed before expiry
- API Calls: Each API request includes the current valid token
Available Operations
1. List Devices
Command: "List all devices" or "Show me my devices"
Tool Called: senza_list_devices
Returns:
- Device ID
- Status (Activated/Registered)
- Community (Alpha/Beta/Stable)
- Description (if set)
Example Response:
| Device ID | Status | Community | Description |
|-------------------|-----------|-----------|------------------|
| 7e6e8eb10d21af04 | Activated | Beta | Elias |
| 7e6e07f20d21af04 | Activated | Stable | Sam |
2. Get Device Details
Command: "When did Elias last connect?" or "Show me device details for 7e6e8eb10d21af04"
Tool Called: senza_get_device
Returns:
- Full device information
- Activation and connection timestamps
- IP address and geolocation
- All device metadata
Use Case: When you need complete information about a specific device.
3. Update Device
Command: "Move device to Beta" or "Update description to 'Living Room'"
Tool Called: senza_update_device
Can Update:
- Community (Alpha/Beta/Stable) - changes release channel
- Description - human-readable label
Example:
User: "Change Elias's device to Beta community"
Claude: [Finds device ID, calls update, confirms change]
Common Workflows
Device Discovery
User: "I see device 7e6e8eb10d21af04 in the logs. Who owns it?"
Claude: [Looks up device, reports description field]
Result: "That's Elias's device, last connected July 22"
Release Management
User: "Move all test devices to Alpha"
Claude: [Lists devices, identifies test devices, updates each]
Result: Multiple devices moved to Alpha channel
Device Audit
User: "Which devices haven't connected in over a month?"
Claude: [Lists devices, checks connection times, reports inactive ones]
Result: List of inactive devices with last connection dates
Troubleshooting
"Device not found" errors:
- Device may be in a different tenant than configured
- Device ID might be incorrect (must be 16 hex characters)
"OAuth authentication failed":
- Check CLIENT_ID and CLIENT_SECRET in config
- Verify credentials in Senza Console → API Keys
- Ensure permissions include
senza:device:readandsenza:device:write
"Forbidden" errors:
- API key doesn't have required permissions
- Enable device read/write when creating the API key
Token Management
Automatic Handling:
- Tokens expire after ~1 hour
- MCP server automatically refreshes 5 minutes before expiry
- No manual intervention needed
What You Don't Have To Do:
- ✗ Copy tokens from browser DevTools
- ✗ Update config every hour
- ✗ Monitor token expiration
- ✓ Just use it!
Best Practices
- Descriptive Device Names: Always set descriptions for easier identification
- Community Management: Use Alpha for testing, Beta for staging, Stable for production
- Regular Audits: Periodically check for inactive devices
- Bulk Operations: Use natural language for batch updates instead of scripting
Comparison: Script vs. MCP
Old Way (Node.js Script):
# 1. Write script
node device-lookup.js input.json output.json
# 2. Edit input.json
vim input.json
# 3. Run again
node device-lookup.js input.json output.json
# 4. Parse output.json
cat output.json | jq '.devices[] | select(.description == null)'
New Way (MCP):
User: "Show me devices without descriptions"
Claude: [Done! Here's the list]
Advantages:
- No scripts to maintain
- No JSON files to manage
- Natural language instead of CLI syntax
- Conversational iteration: "Now update those with owner names"
Saved to: /Users/yourname/Projects/senza-devices-workflow.md
This document covers the architecture, available operations, common workflows, and troubleshooting. Keep it handy as a reference!
User: Marvelous.
Claude: Thanks! You've now got a complete MCP server that lets you manage Senza devices through conversation.
Here's what makes this powerful:
- No Context Switching: Stay in Claude, don't jump to terminals or scripts
- Natural Language: Ask in English, not bash or Python
- Iterative: "Show devices" → "Filter by Beta" → "Update those descriptions"
- Self-Documenting: The conversation is the documentation
The pattern we used here - OAuth authentication, API client, tool registration - works for any API. You could build similar MCP servers for:
- Analytics platforms (BigQuery, MongoDB)
- Content management systems
- Customer databases
- IoT device fleets
- Cloud infrastructure
Once you've built an MCP server, managing those systems becomes as easy as having a conversation.
Want to explore other MCP servers or build more tools for Senza? Just ask!
Summary
In this tutorial, we built an MCP server that provides natural language access to the Senza Devices API. Instead of writing scripts to manage devices, we can now simply ask Claude to:
- List all devices in a tenant
- Get detailed device information
- Update device properties
Key Concepts:
- MCP (Model Context Protocol): A standard for connecting Claude to external tools and APIs
- Tools: Functions that Claude can call on your behalf
- OAuth Authentication: Secure, automatic token management
- Natural Language Interface: Ask questions instead of writing code
Resources:
- MCP Server Source: https://github.com/synamedia-senza/senza-devices-mcp-server
- Original Script Tutorial: https://developer.synamedia.com/senza/docs/device-lookup
- Senza Developer Docs: https://developer.synamedia.com/senza/docs
Next Steps:
- Try building MCP servers for other APIs
- Combine multiple MCP servers for powerful workflows
- Explore the MCP SDK documentation at https://modelcontextprotocol.io
Happy building! 🚀
Updated 4 days ago