MCP Server

Let Claude, ChatGPT, or any MCP-compatible AI agent read sensors and control devices on your jettyd fleet via OAuth 2.0.

Overview

The jettyd MCP server exposes your IoT fleet as a set of tools consumable by large language models. It runs at https://mcp.jettyd.com and implements the Model Context Protocol over HTTP with Server-Sent Events (SSE). Authentication uses OAuth 2.0 with PKCE — no client secrets required.

Hosted vs. self-hosted The hosted MCP server at mcp.jettyd.com connects to your jettyd account via OAuth. If you need a locally-running server for Claude Desktop with an API key instead of OAuth, see the @jettyd/mcp npm package.

OAuth PKCE Flow

The hosted MCP server uses OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange). No client secret is needed — PKCE provides the security guarantee instead.

Step 1 — Discovery

Fetch the authorization server metadata to get all endpoint URLs:

curl https://mcp.jettyd.com/.well-known/oauth-authorization-server

The server returns a JSON document with the endpoints your client needs:

{
  "issuer": "https://auth.jettyd.com",
  "authorization_endpoint": "https://auth.jettyd.com/oauth/authorize",
  "token_endpoint": "https://auth.jettyd.com/oauth/token",
  "introspection_endpoint": "https://auth.jettyd.com/oauth/introspect",
  "scopes_supported": [
    "devices:read", "devices:write",
    "telemetry:read", "commands:write",
    "rules:read", "rules:write"
  ],
  "response_types_supported": ["code"],
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"]
}

Step 2 — Authorization

Generate a PKCE code verifier and challenge, then redirect the user to the authorization endpoint:

# Generate a high-entropy code verifier (43-128 chars, URL-safe)
CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '+/=\n' | head -c 64)

# Derive the S256 code challenge
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
  | openssl dgst -sha256 -binary \
  | openssl base64 \
  | tr -d '=' | tr '+/' '-_')

# Open the authorization URL in the user's browser
open "https://auth.jettyd.com/oauth/authorize?\
response_type=code\
&client_id=YOUR_CLIENT_ID\
&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback\
&scope=devices%3Aread+telemetry%3Aread+commands%3Awrite\
&code_challenge=${CODE_CHALLENGE}\
&code_challenge_method=S256\
&state=$(openssl rand -hex 16)"
Validate the state parameter Always generate a random state value and verify it matches when the authorization server redirects back to your redirect_uri. This prevents CSRF attacks.

After the user approves access, the authorization server redirects to your redirect_uri with a short-lived code parameter:

http://localhost:8080/callback?code=AUTH_CODE_HERE&state=YOUR_STATE

Step 3 — Token Exchange

Exchange the authorization code for an access token. Include the original code_verifier — the server verifies it against the challenge from Step 2:

curl -X POST https://auth.jettyd.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE_HERE" \
  -d "redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code_verifier=${CODE_VERIFIER}"

On success the server returns:

{
  "access_token": "at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_...",
  "scope": "devices:read telemetry:read commands:write"
}
Token refresh Access tokens expire after one hour. Use the refresh_token with grant_type=refresh_token at the token endpoint to obtain a new access token without requiring user interaction again.

Client Configuration

Claude Desktop

Add the jettyd MCP server to your Claude Desktop configuration file. On macOS the config is at ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows at %APPDATA%\Claude\claude_desktop_config.json.

Option A — Hosted OAuth server (recommended for shared / multi-user setups):

{
  "mcpServers": {
    "jettyd": {
      "type": "sse",
      "url": "https://mcp.jettyd.com/mcp"
    }
  }
}

Claude Desktop will open a browser window for OAuth consent on first use and store the resulting token automatically.

Option B — Local npx server with API key (faster for single-user dev):

{
  "mcpServers": {
    "jettyd": {
      "command": "npx",
      "args": ["-y", "@jettyd/mcp"],
      "env": {
        "JETTYD_ACCESS_TOKEN": "at_YOUR_TOKEN_HERE",
        "JETTYD_API_URL": "https://api.jettyd.com"
      }
    }
  }
}
Restart Claude Desktop after editing Configuration changes take effect only after a full quit and relaunch — File → Quit, not just closing the window.

Claude.ai (web)

Claude.ai supports adding remote MCP servers directly in the browser. No local installation required.

  1. Open Settings → Integrations in Claude.ai.
  2. Click Add integration and enter the server URL: https://mcp.jettyd.com/mcp
  3. Click Connect. You will be redirected to jettyd's OAuth consent screen.
  4. Approve the requested scopes. Claude.ai stores the token and reconnects automatically on future sessions.
Scope selection Request only the scopes your workflow needs. Claude.ai will show users exactly what permissions they are granting. Requesting commands:write prominently signals that the integration can actuate devices.

ChatGPT and other MCP clients

Any client that supports the MCP specification can connect to the jettyd hosted server. Use the SSE endpoint with Bearer token authentication:

# Test connectivity with curl after completing OAuth
curl -H "Authorization: Bearer at_YOUR_TOKEN_HERE" \
  -H "Accept: text/event-stream" \
  https://mcp.jettyd.com/mcp

For clients that require a static URL with the token embedded (non-standard, use with care):

https://mcp.jettyd.com/mcp?access_token=at_YOUR_TOKEN_HERE
Query-string tokens Passing the token as a query parameter is supported for legacy clients but is less secure — the token appears in server logs and browser history. Prefer the Authorization: Bearer header whenever possible.

Tool Reference

The MCP server exposes eight tools. Each tool call is validated server-side; requests that exceed the token's granted scopes are rejected with 403 Forbidden.

Tool Required scope(s) Description
list_devices devices:read Return all devices in the tenant fleet, including online/offline status and last-seen timestamp.
get_device devices:read Fetch full metadata for a single device by ID: firmware version, shadow state, assigned tags, and connection info.
get_fleet_status devices:read Aggregate fleet health summary: total devices, online count, devices with active alerts, and last-update time.
read_sensor telemetry:read Read the latest telemetry value for a specific sensor key on a device. Returns value, unit, and timestamp.
get_history telemetry:read Query historical telemetry for a device and sensor key over a time range. Returns a time-series array.
send_command commands:write Send an actuator command to a device (e.g., toggle relay, set setpoint). Command is delivered via MQTT and acknowledged.
configure_device devices:write Update device shadow configuration (reporting interval, thresholds, tags). Change is persisted and synced to the device.
list_alerts rules:read List all alert rules for the tenant, including trigger conditions, notification channels, and current fired/resolved state.

Example tool calls

list_devices — no arguments required:

{}

read_sensor — read temperature from a specific device:

{
  "device_id": "dev_01hx3k9j2m4v5p6q7r8s",
  "sensor_key": "temperature"
}

get_history — fetch the last 24 hours of humidity readings:

{
  "device_id": "dev_01hx3k9j2m4v5p6q7r8s",
  "sensor_key": "humidity",
  "from": "2025-05-19T00:00:00Z",
  "to": "2025-05-20T00:00:00Z",
  "resolution": "1h"
}

send_command — turn on a relay:

{
  "device_id": "dev_01hx3k9j2m4v5p6q7r8s",
  "command": "set_relay",
  "params": {
    "channel": 1,
    "state": "on"
  }
}

configure_device — change reporting interval to 60 seconds:

{
  "device_id": "dev_01hx3k9j2m4v5p6q7r8s",
  "config": {
    "telemetry_interval_s": 60
  }
}

OAuth Scopes

Request only the scopes your integration requires. Users see the scope list on the consent screen; narrower scope requests build more trust.

Scope Grants access to
devices:read Read device metadata, online status, shadow state, firmware version, and tags. Required by list_devices, get_device, and get_fleet_status.
devices:write Update device shadow configuration, reporting intervals, and tags. Required by configure_device. Does not grant ability to send actuator commands.
telemetry:read Read current and historical sensor values. Required by read_sensor and get_history. Scoped to devices the user owns or has been shared access to.
commands:write Send actuator commands to devices (relay, setpoint, motor, etc.). Required by send_command. All commands are logged with the token's subject for audit purposes.
rules:read Read alert rules, trigger conditions, and notification channel configuration. Required by list_alerts.
rules:write Create, update, and delete alert rules and notification channels. No MCP tool currently requires this scope — it is reserved for future rule-management tools.

Error Responses

The MCP server returns standard HTTP status codes alongside a JSON error body on all failure paths.

401 Unauthorized

Returned when the request cannot be authenticated. Two distinct causes:

Missing token — no Authorization header or access_token query parameter was provided:

{
  "error": "unauthorized",
  "error_description": "Bearer token required. Authenticate via OAuth before calling MCP tools."
}

Invalid or expired token — the token is malformed, has been revoked, or has expired:

{
  "error": "invalid_token",
  "error_description": "The access token is invalid or has expired. Obtain a new token via the OAuth flow."
}

403 Forbidden

Insufficient scope — the token is valid but does not include the scope required by the called tool (RFC 6750 §3.1):

{
  "error": "insufficient_scope",
  "error_description": "This tool requires the 'commands:write' scope. Re-authorize with the additional scope.",
  "scope": "commands:write"
}

503 Service Unavailable

Returned when the token introspection endpoint is unreachable and the server cannot verify the token. The MCP server fails closed — it does not grant access on introspection failure:

{
  "error": "introspection_unavailable",
  "error_description": "Token introspection is temporarily unavailable. Please retry in a moment."
}
Fail-closed behaviour When introspection_unavailable is returned, the server has rejected your request. This is intentional — jettyd never grants access on an ambiguous auth result. Retry after a short backoff; if the error persists, check the status page.