Documentation

MCP Server

AVstackr exposes 258 tools via the Model Context Protocol — manage projects, products, customers, rooms, equipment, schematics, AI features, and external record storage from any MCP-compatible client.

Overview

AVstackr's MCP (Model Context Protocol) server gives AI tools direct access to your projects, products, customers, schematics, AI features, and external record storage — no REST API calls required. 258 tools are available covering the full project lifecycle.

When to use MCP vs REST API: Use MCP when connecting AI tools that support it natively (Claude Desktop, claude.ai, Cursor, etc.). Use the REST API for traditional integrations or when you need more control over the request/response cycle.

Requirements

Requirement Details
Endpoint https://avstackr.com/mcp
Transport Streamable HTTP
Authentication OAuth 2.1 (Claude Desktop) or API key (avs_...)
Subscription Active paid subscription required

Connect with Claude Desktop (Recommended)

The fastest way to connect — no bridge script or API key needed. Claude Desktop connects directly via OAuth.

  1. Open Claude Desktop and go to Settings
  2. Navigate to Integrations
  3. Click Add and paste: https://avstackr.com/mcp
  4. Complete the OAuth login — you'll be redirected to AVstackr to authorize
  5. All 258 tools appear automatically

For a full walkthrough with video, see the Claude Desktop Setup guide.

Connect Other MCP Clients

For clients that don't support OAuth, authenticate with an API key passed as a Bearer token in the Authorization header.

Step 1: Create an API Key

Go to Settings → API Keys → Create API Key. Give it a name and copy the key. It starts with avs_ and won't be shown again.

HTTP vs stdio clients: If your MCP client supports Streamable HTTP transport, point it directly at https://avstackr.com/mcp with the API key as a Bearer token — no bridge needed. If your client only supports stdio transport (e.g., Cursor, some custom agents), you'll need the bridge script below to translate between stdio and HTTP.

Step 2: Install the Bridge Script (stdio clients only)

The bridge translates between stdio (stdin/stdout) and Streamable HTTP. Copy and save as ~/mcp-http-bridge.js. Requires Node.js v18+.

Click to show mcp-http-bridge.js — copy and save as ~/mcp-http-bridge.js
#!/usr/bin/env node

const http = require('http');
const https = require('https');
const readline = require('readline');

const MCP_URL = process.env.MCP_URL;
const MCP_AUTH = process.env.MCP_AUTH;
const MCP_INSECURE = process.env.MCP_INSECURE === '1';

const debug = (msg) => process.stderr.write(`[bridge] ${msg}\n`);

if (!MCP_URL) { debug('MCP_URL not set, exiting'); process.exit(1); }
debug(`Starting bridge to ${MCP_URL}`);

const url = new URL(MCP_URL);
const isHttps = url.protocol === 'https:';
const transport = isHttps ? https : http;
const defaultPort = isHttps ? 443 : 80;

let sessionId = null;
let getStreamReq = null;
let lastEventId = null;
let shuttingDown = false;
const BACKOFF_START_MS = 1000;
const BACKOFF_MAX_MS = 30000;
let backoffMs = BACKOFF_START_MS;

function buildCommonHeaders(extra = {}) {
  const h = { 'Accept': 'application/json, text/event-stream', ...extra };
  if (MCP_AUTH) h['Authorization'] = MCP_AUTH;
  if (sessionId) h['Mcp-Session-Id'] = sessionId;
  return h;
}

function buildRequestOptions(method, headers) {
  const opts = {
    hostname: url.hostname, port: url.port || defaultPort,
    path: url.pathname + url.search, method, headers
  };
  if (isHttps && MCP_INSECURE) opts.rejectUnauthorized = false;
  return opts;
}

function writeToStdout(json) { process.stdout.write(json + '\n'); }

function parseSseChunk(buffer) {
  const events = []; let leftover = buffer;
  while (true) {
    const b = leftover.indexOf('\n\n');
    if (b === -1) break;
    const raw = leftover.slice(0, b); leftover = leftover.slice(b + 2);
    let id = null; const dataLines = [];
    for (const line of raw.split('\n')) {
      if (line.startsWith('id:')) id = line.slice(3).trim();
      else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
    }
    if (dataLines.length > 0) events.push({ id, data: dataLines.join('\n') });
  }
  return { events, leftover };
}

function emitSseEvent(ev) {
  if (ev.id) lastEventId = ev.id;
  if (!ev.data) return;
  try { JSON.parse(ev.data); writeToStdout(ev.data); }
  catch { debug(`Dropping non-JSON SSE data`); }
}

// --- Session recovery (exponential backoff, retries indefinitely) ---
let reinitializing = false;
let resetBackoffMs = BACKOFF_START_MS;

function resetSession() {
  if (reinitializing || shuttingDown) return;
  reinitializing = true;
  debug(`Session lost — will re-initialize in ${resetBackoffMs}ms`);
  sessionId = null; lastEventId = null; backoffMs = BACKOFF_START_MS;
  if (getStreamReq) { try { getStreamReq.destroy(); } catch {} getStreamReq = null; }
  setTimeout(() => {
    reinitializing = false;
    const initMsg = {
      method: 'initialize',
      params: { protocolVersion: '2025-11-25', capabilities: {},
                clientInfo: { name: 'mcp-http-bridge', version: '1.0.0' } },
      jsonrpc: '2.0', id: `reinit-${Date.now()}`
    };
    handleClientMessage(initMsg);
    resetBackoffMs = Math.min(resetBackoffMs * 2, BACKOFF_MAX_MS);
  }, resetBackoffMs);
}

function isSessionLost(statusCode, body) {
  if (statusCode === 404) return true;
  if (statusCode === 403 && body && body.includes('does not match')) return true;
  return false;
}

// --- GET SSE stream ---
function openGetStream() {
  if (shuttingDown || !sessionId || getStreamReq) return;
  const headers = buildCommonHeaders();
  if (lastEventId) headers['Last-Event-ID'] = lastEventId;
  const options = buildRequestOptions('GET', headers);

  const req = transport.request(options, (res) => {
    if (res.statusCode === 405) { getStreamReq = null; return; }
    if (res.statusCode === 404 || res.statusCode === 403) {
      res.resume(); getStreamReq = null; resetSession(); return;
    }
    if (res.statusCode !== 200) { res.resume(); scheduleReconnect(); return; }
    backoffMs = BACKOFF_START_MS;
    let buffer = '';
    res.on('data', (chunk) => {
      buffer += chunk.toString('utf8');
      const { events, leftover } = parseSseChunk(buffer);
      buffer = leftover;
      for (const ev of events) emitSseEvent(ev);
    });
    res.on('end', () => { getStreamReq = null; scheduleReconnect(); });
    res.on('error', () => { getStreamReq = null; scheduleReconnect(); });
  });
  req.on('error', () => { getStreamReq = null; scheduleReconnect(); });
  req.end(); getStreamReq = req;
}

function scheduleReconnect() {
  if (shuttingDown || !sessionId) return;
  setTimeout(() => { backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS); openGetStream(); }, backoffMs);
}

// --- POST handling ---
function handleClientMessage(request) {
  const isNotification = request.id === undefined;
  const postData = JSON.stringify(request);
  const headers = buildCommonHeaders({
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(postData)
  });
  const options = buildRequestOptions('POST', headers);

  const req = transport.request(options, (res) => {
    const returnedSession = res.headers['mcp-session-id'];
    if (returnedSession && !sessionId) {
      sessionId = returnedSession;
      resetBackoffMs = BACKOFF_START_MS;
      debug(`Captured session id: ${sessionId}`);
      openGetStream();
    }
    if (res.statusCode === 202) { res.resume(); return; }
    const ct = (res.headers['content-type'] || '').toLowerCase();
    if (ct.includes('text/event-stream')) {
      let buffer = '';
      res.on('data', (chunk) => {
        buffer += chunk.toString('utf8');
        const { events, leftover } = parseSseChunk(buffer);
        buffer = leftover;
        for (const ev of events) emitSseEvent(ev);
      });
    } else {
      let data = '';
      res.on('data', (chunk) => { data += chunk; });
      res.on('end', () => {
        if (!data.trim()) return;
        if (isSessionLost(res.statusCode, data)) { resetSession(); return; }
        try { JSON.parse(data); writeToStdout(data.trim()); } catch {}
      });
    }
  });
  req.on('error', (e) => {
    const detail = `${e.code || 'ERR'}: ${e.message || '(no message)'}`;
    if (!isNotification) writeToStdout(JSON.stringify({
      jsonrpc: '2.0', id: request.id, error: { code: -32000, message: detail }
    }));
  });
  req.write(postData); req.end();
}

// --- Session termination ---
function terminateSession(cb) {
  if (!sessionId) { cb && cb(); return; }
  const headers = buildCommonHeaders();
  const opts = buildRequestOptions('DELETE', headers);
  const req = transport.request(opts, (res) => { res.resume(); res.on('end', () => cb && cb()); });
  req.on('error', () => cb && cb()); req.end();
}

// --- stdin loop ---
const rl = readline.createInterface({ input: process.stdin, terminal: false });
rl.on('line', (line) => {
  try { handleClientMessage(JSON.parse(line)); } catch (e) { debug(`stdin parse error: ${e.message}`); }
});
rl.on('close', () => shutdown());

function shutdown() {
  if (shuttingDown) return; shuttingDown = true;
  if (getStreamReq) { try { getStreamReq.destroy(); } catch {} getStreamReq = null; }
  terminateSession(() => process.exit(0));
  setTimeout(() => process.exit(0), 2000).unref();
}
process.on('SIGTERM', () => shutdown());
process.on('SIGINT', () => shutdown());

The bridge requires Node.js (v18+). No npm install needed — it uses only built-in modules.

Step 3: Configure Your Client

Add the following to your MCP client config (example: claude_desktop_config.json):

{
    "mcpServers": {
        "avstackr": {
            "command": "node",
            "args": ["~/mcp-http-bridge.js"],
            "env": {
                "MCP_URL": "https://avstackr.com/mcp",
                "MCP_AUTH": "Bearer avs_YOUR_API_KEY_HERE"
            }
        }
    }
}

Replace avs_YOUR_API_KEY_HERE with your API key from Step 1.

Step 4: Restart Your Client

After saving the config, restart your MCP client. All 258 tools should appear.

Troubleshooting

Check the MCP log at ~/Library/Logs/Claude/mcp*.log (macOS) for errors.

Symptom Fix
ECONNREFUSED Server is not running or wrong URL
401 / auth error Invalid or revoked API key — generate a new one from Settings
403 subscription error Your account doesn't have an active subscription
No tools showing Check that the bridge script path is correct and Node.js is installed

Available Tools

Once connected, your MCP client will see the following tools:


TOOL search_knowledge_base

Search manufacturer documentation using natural language. Returns articles ranked by relevance using vector similarity.

Parameter Type Required Description
query string Yes Natural language search query
manufacturer string No Filter by manufacturer slug (e.g. biamp, crestron)
category string No Filter by article category
limit integer No Max results to return (default: 5)

Example Response

{
    "count": 3,
    "results": [
        {
            "id": 42,
            "title": "TesiraFORTE Network Configuration",
            "summary": "Step-by-step guide for configuring network settings...",
            "content_snippet": "The TesiraFORTE connects via Dante...",
            "similarity": 0.891,
            "source_url": "https://support.biamp.com/...",
            "category": "network",
            "product_line": "tesira",
            "manufacturer_name": "Biamp"
        }
    ]
}

TOOL get_knowledge_base_entry

Get the full content of a knowledge base article by ID. Use this after searching to retrieve the complete article text.

Parameter Type Required Description
id integer Yes Article ID from search results

Example Response

{
    "id": 42,
    "title": "TesiraFORTE Network Configuration",
    "summary": "Step-by-step guide for configuring network settings...",
    "content": "Full article text with configuration details...",
    "source_url": "https://support.biamp.com/...",
    "category": "network",
    "product_line": "tesira",
    "tags": ["dante", "networking", "configuration"],
    "manufacturer": "Biamp"
}

TOOL list_knowledge_base_manufacturers

List all available manufacturer knowledge bases. Use this to discover which manufacturers have documentation before searching.

Parameters: None

Example Response

{
    "count": 2,
    "manufacturers": [
        {
            "slug": "biamp",
            "display_name": "Biamp",
            "description": "Tesira, Devio, Parle, Vocia product families",
            "article_count": 1292
        },
        {
            "slug": "crestron",
            "display_name": "Crestron",
            "description": "Control systems, SIMPL programming, hardware",
            "article_count": 3533
        }
    ]
}

Typical Workflow

  1. Call list_knowledge_base_manufacturers to see available knowledge bases
  2. Call search_knowledge_base with a natural language question and optional manufacturer filter
  3. Call get_knowledge_base_entry with an article ID to get the full content

Error Responses

Scenario Error
No JWT token provided Connection rejected — valid JWT Bearer token required
No active subscription Connection rejected — active subscription required
Article not found {"error": "Entry not found"}

TOOL get_schematic

Get the project's signal-flow schematic as a shareable viewer URL. Returns a 30-day signed link that renders the saved schematic as a PDF in any browser, with the title block exactly as the user configured it (status, revisions, drawn-by, project number, notes — everything). Use when the caller asks to see, share, or download a project's schematic. Each project has at most one schematic; this tool resolves it from the project id.

Parameter Type Required Description
project_id integer Yes The project ID. Use list_projects to find it by name, job number, or customer.

Example Response

{
    "success": true,
    "project_id": 431,
    "schematic_id": 51,
    "page_count": 1,
    "device_count": 12,
    "has_layout": true,
    "view_url": "https://app.avstackr.com/schematic/eyJ...",
    "message": "Schematic retrieved — open view_url in a browser to view or download as PDF."
}

When the project has no saved schematic, the response is { "error": "No schematic found for project ..." }.


TOOL get_schematic_image

Render the project's signal-flow schematic to PNG(s) — the same canvas the browser editor draws, captured server-side — so an AI agent can visually verify a layout it built (spot out-of-bounds bundles, check spacing, read device labels) instead of reasoning from JSON alone. Every response includes a text block with a 30-day signed viewer link (open the full zoomable drawing in a browser — share this with a human). Page defaults to 1 and is capped to the schematic's real page count.

Three modes trade coverage for detail:

  • Overview (default) — the whole page as one image, roughly 44 pixels per drawing inch. Best for gross layout, out-of-bounds bundles, and spacing.
  • Region zoom — pass all four region_x, region_y, region_width, region_height (page inches, top-left origin over the 36×24 sheet) to spend the pixel budget on one rectangle. Smaller regions win: an 8×6-inch corner reaches about 173 px/in (roughly 4× the overview), an 18×12-inch half-sheet about 83 px/in. Use it to read labels and wire numbers the overview can't resolve. Returns 1 text block + 1 image.
  • Tiled sweep — pass tiled: true to render a 3×2 overlapping grid at about 101 px/in (roughly 2.3× the overview). The response leads with a geometry-index text block (per-tile page-inch bounds, so any observation maps back to sheet inches), then alternates TILE n text and image blocks for the 6 tiles. A sweep costs about 16.5k visual tokens, so reach for it deliberately — end-of-work verification, not every iteration.
Parameter Type Required Description
project_id integer Yes The project ID. Use list_projects to find it by name, job number, or customer.
page integer No 1-indexed page to render. Defaults to 1; capped to the schematic's page count.
region_x number No Region zoom: left edge in page inches (top-left origin over the 36×24 sheet). All four region params are all-or-none.
region_y number No Region zoom: top edge in page inches.
region_width number No Region zoom: width in page inches. Must be at least 2 inches after clamping to the sheet.
region_height number No Region zoom: height in page inches. Must be at least 2 inches after clamping to the sheet.
tiled boolean No Set true for the 3×2 overlapping tiled sweep with a geometry index. Cannot be combined with the region params.

Give all four region params or none — a partial region, or a region combined with tiled: true, returns an explanatory text block instead of an image. When the project has no saved schematic, the tool returns a single text block: No schematic found for project ....


TOOL get_project_excel_url

Mint a 7-day signed URL to download the project's Excel workbook (.xlsx). Use when the caller asks to download, send, or share a project's Excel export. All formatting options are optional — omit any to fall back to the project's saved default, then the system default.

Parameter Type Required Description
project_id integer Yes The project ID. Use list_projects or ai_semantic_search to find it by name, job number, or customer.
mode string No FullProject (default) for the rich workbook, or EquipmentOnly for a simple six-column equipment list.
include_vendor_tabs boolean No Include the Vendor sheets.
include_vendor_po_tabs boolean No Include the Vendor PO sheets.
include_bom_combined_tab boolean No Include the BOM Combined sheet (flat bill of materials across the project).
include_financial_analysis_tab boolean No Include the Financial Analysis sheet (cost vs. sell with margins).
hide_notes_column boolean No Hide the Notes column on per-room sheets (cleaner printouts).
include_part_number_column boolean No Add a Part# column to per-room and Vendor sheets.
include_formulas boolean No Set false to render numbers as static values (useful when the consumer can't recalculate Excel formulas).

Example Response

{
    "success": true,
    "project_id": 42,
    "url": "https://yourdomain.com/project-excel/<token>?mode=FullProject",
    "expires_at": "2026-05-26T14:32:08Z",
    "filename": "Acme HQ - 2026-05-19.xlsx",
    "message": "Excel export URL minted — open url in a browser to download the .xlsx file. Link expires in 7 days."
}

For the user-workflow guide (what the workbook contains, how saved defaults work), see Excel Export. For the equivalent REST API endpoint with the full PascalCase parameter table, see Projects API.


Available Tools

All tools require authentication (OAuth or API key) and an active subscription.

Projects

ToolDescription
list_projectsList or search projects by name, job number, or customer
get_projectGet project details with rooms and equipment counts
create_projectCreate a new project with optional customer
update_projectUpdate any project field in one step — name, job number, personnel, pipeline stage, workflow status, customer link, and the contact, Bill To, Ship To, and job-site addresses. Bill To and Ship To print on generated estimates and invoices; status changes are recorded in the project's history. Pass customer_id: 0 to unlink the customer
delete_projectDelete a project permanently
duplicate_projectCopy a project with all rooms and equipment
get_project_financialsGet project financials (equipment, services, tax, total)
get_scope_notesGet scope call notes and timeline notes
get_equipment_summaryGet bill of materials across all rooms
get_schematicGet the project's signal-flow schematic as a shareable viewer link
get_schematic_imageRender a schematic page to PNG(s) so an agent can see the layout it built — whole-page overview, a zoomed region, or a tiled sweep, each with a viewer link
get_project_excel_urlMint a 7-day signed URL to download the project's Excel workbook (.xlsx)
list_versionsList all versions for a project (Draft / Contracted / Archived)
get_versionGet a single version by ID
branch_versionCreate a new draft version branched from an existing version
activate_versionActivate an existing Draft or Contracted version. Fails on Archived — use promote_version instead
promote_versionBring an Archived version back as a new Draft and auto-activate it
get_contractGet the contract on a version (frozen state at approval)
get_projectionGet the projected state of a version (Contract + all approved Change Orders applied)
list_estimates_for_projectList all saved estimates for a project
get_estimateGet a saved estimate by ID with a fresh signed view URL
approve_estimateApprove a saved estimate (creates the Contract, flips its Version to Contracted, auto-archives Draft siblings). Idempotent — re-approving returns the existing Contract with already_approved=true. Pass project_id; estimate_id optional (defaults to the latest saved estimate on the active version)
void_estimateUndo an approved estimate (deletes the Contract, flips the Version back to Draft). Blocked when the contract has activity — error void_blocked includes counts of approved Change Orders and live invoices. Idempotent: voiding an already-Draft estimate returns already_voided=true. Pass project_id; estimate_id optional
delete_estimateDelete a saved estimate by ID. Blocked (estimate_approved) if approved — void the approval first.
list_invoices_for_projectList all saved invoices for a project
get_invoiceGet a saved invoice by ID with a fresh signed view URL
void_invoiceVoid a saved invoice by ID — rolls back its billing allocations so it can be deleted. Idempotent. Optional reason for the audit trail.
delete_invoiceDelete a saved invoice by ID. Blocked (invoice_has_live_allocations) if it has live allocations — void it first.

Rooms & Equipment

ToolDescription
list_roomsList all rooms in a project
add_roomAdd a room to a project
update_roomRename a room
delete_roomDelete a room and all its equipment
get_room_equipmentList equipment in a specific room
add_line_itemAdd a product to a room
update_line_itemFull per-line editor: quantity, cost, override %, labor/programming/configuration hours, equipment source, notes, snapshot overrides (make/model/part/description), and cable assignments via cables_json. Sparse — only supplied fields change. Set clear_override_percent: true to reset markup to the tenant default.
delete_line_itemRemove equipment from a room
replace_room_equipmentPush a room's COMPLETE equipment list in one go: lines that match are updated in place (keeping their id, notes and hours), lines you left out are deleted, and anything new is added. Read the room with get_room_equipment first — a partial list empties the rest. Send either items_json (a flat list) or sections_json (named groups that become the room's dividers); items_json='[]' empties the room. Every item needs a quantity — there is no default, so a line that omits it is rejected rather than quietly set to 1. Nothing is written unless every item passes validation. Schematic devices are never deleted — a device left with no equipment behind it comes back in orphaned_canvas_devices so you can flag it.
replace_project_equipmentThe same replace-all across several rooms at once, via rooms_json. A room you list is replaced wholesale; a room you omit is left completely untouched. Either every room succeeds or none do. Limits: 100 rooms per request, 500 items per room, 5,000 items total.
copy_room_equipmentDuplicate one room's equipment into another room of the same project, matched by exact catalog product ID — nothing is name-searched, so nothing can be substituted. Carries quantities, labor/programming/configuration hours, costs and markups, notes, equipment source, cable counts, the make/model/part snapshots, the owner-furnished flag, and the source room's section headings, appended after whatever the target room already holds. Does NOT carry cable-schedule rows, line-item attributes, PO numbers or quote references. A product the target room already has merges by adding its quantity ONLY — no other field on that row is touched. Headings never merge, so copying the same room twice leaves duplicate headings. Copying a room into itself is refused.
add_dividerAdd a category section to a room's equipment list — a labeled header that groups the line items beneath it into a subtotaled group in the estimate. These labels appear as category in get_room_equipment. Examples: 'Displays', 'Audio', 'Control'. Placement: pass above_line_item_id to put the section directly above that line item (it wins over order), pass order for an explicit position, or omit both to append at the end. Any placement pushes every item and section at or below the target down one, so order values from an earlier read go stale — re-read the room before reusing them. Anchoring above the first line of an existing section empties that section: it stays a bare heading row on estimates and never becomes a band on proposals, so rename or delete it if you meant to relabel. On success returns the room's resulting sequence (null if the read-back failed — re-read the room) plus sections with a per-section item count, empty_sections and unlabelled_item_count. Not the visual room divider on the schematic canvas — for that use add_schematic_divider.
list_dividerList a room's category sections (the groupings shown as category in get_room_equipment) and their order.
update_dividerRename a category section or move it within the equipment list. Placement: pass above_line_item_id to move the section directly above that line item (it wins over order), pass order for an explicit position, or omit both to leave the position alone. A move pushes every item and section at or below the target down one, so order values from an earlier read go stale — re-read the room before reusing them. Moving a section directly above the first line of another section empties that one: it still renders as a bare heading row on estimates, and never becomes a band on proposals. This moves ONLY the heading row — the items it labels stay where they are, and join whatever section then sits above them, often the one above its old spot. To move a whole section (its heading and its items together) use move_section instead. On success returns the room's resulting sequence (null if the read-back failed — re-read the room) plus sections with a per-section item count, empty_sections and unlabelled_item_count.
delete_dividerRemove a category section. The equipment under it stays — those items regroup under the section above.
move_sectionMove a whole section of a room's equipment list — the category heading AND every row under it — as one block. placement is top | end | above | below, where above and below are relative to another section's heading and REQUIRE target_divider_id (that other section's divider id); top and end ignore it. Get divider ids from list_divider. top puts the section above every other heading but leaves any unlabelled rows — rows sitting above the first heading — where they are. A move that would change the order is refused on a contract-locked version; a no-op is not, because nothing is written. On success returns the room's resulting sequence (null if the read-back failed — re-read the room) plus sections with a per-section item count, empty_sections and unlabelled_item_count.
reorder_roomReorder a room's equipment list — the tool for ROW-level moves. To move a whole section, its heading and every row under it, use move_section instead. Provide the room's COMPLETE new sequence as entries_json — every line item and category-section divider currently in the room, each exactly once. Extras, omissions or duplicates are rejected and nothing changes. A sequence that would leave a category heading with no items under it is refused unless allow_empty_sections is set. Read the current ids/order first with get_room_equipment (line items) and list_divider (dividers). On success returns the resulting sequence (null if the read-back failed — re-read the room). Refused on a contract-locked version.
list_cable_typesList active cable types in the tenant's catalog (id, name, sell/cost per foot, sell/cost per run). Use this to find cable_type_id values for update_line_item's cables_json. Pass include_inactive=true to also see deactivated types.

Products

ToolDescription
search_productsSearch the catalog by name, model, part number, or plain description. The tool routes itself — exact match on model/part first, then keyword, then meaning — so send the query as it was given and don't classify it. Returns active products only. The meaning pass costs 1 AI credit and runs only when the earlier stages come up short or the query reads as a description; exact and keyword matches are free. Each row carries a leg naming the stage that found it: fuzzy is the closest spelling, not a match, so offer those as "did you mean" rather than as the product asked for.
get_productGet product details including connection metadata
create_productAdd a new product to the catalog
update_productUpdate product details (make, model, price, etc.)
update_product_metadataReplace connection metadata (ports, category, device type, power). Omitted ports/power/notes keep their current values; supplied ports replace wholesale. Each port carries poeSource (the tier it supplies, or "none") and poeDraw (the tier it draws) — set at most one of the two. Both are checked against the port registry and silently saved as null on a connector that can't carry PoE; a port sent with both keeps poeDraw. The device's power is derived from the jack marked PD when it has one, so a tier you pass is overwritten by the strongest tier any jack draws.
price_lookupLook up pricing by model or part number
list_port_typesList valid port types for connection metadata

Custom Device Blocks

A custom device block is imported artwork that draws in place of a schematic device's standard rectangle — the footprint and ports stay the standard block's, only the picture changes. Agents import artwork by sending a PNG or PDF base64-encoded in content_base64, at most 1.5 MB once decoded (the in-app library at Schematics → Custom Blocks takes up to 50 MB for larger sources). The format is read from the file's own bytes, never its name, so send the original file unmodified. Artwork is auto-cropped to its visible content and, by default, thresholded to black strokes on transparency so it tints white on the dark canvas and prints as-is on white paper.

ToolDescription
list_custom_blocksList the company's blocks with id, name, aspect ratio, line-art flag, and archived state. Archived blocks are hidden unless include_archived=true
create_custom_blockImport a PNG or PDF as a new block. Names must be unique among active blocks. The response includes a rendered preview, so the agent sees the artwork it just made
get_custom_block_imageReturn one block's processed artwork as a PNG alongside its details. Archived blocks still render
rename_custom_blockRename a block. Drawings already using it keep the same artwork — only the library label changes
set_custom_block_archivedRetire a block from the picker, or restore it. Artwork already on a schematic keeps rendering. There is no delete — archiving is the removal
link_product_custom_blockPin a block to a catalog product, or omit block_id to unpin. Future placements of that product arrive with the artwork; existing placements are untouched

Customers

ToolDescription
list_customersList all customers
get_customerGet customer details
create_customerCreate a new customer
update_customerUpdate customer company, contact, email, phone, fax, website, billing/shipping addresses, payment terms, and notes
delete_customerDelete a customer

AI Features

ToolDescriptionCredits
ai_semantic_searchNatural language product search using embeddingsFree
ai_match_equipmentMatch an equipment list against the catalogFree
ai_parse_scopeParse scope notes into product matchesFree
ai_executive_summaryGenerate AI-powered project executive summary. Pass save=true to also persist the result onto the project's active version so proposal templates pick it up.10
set_executive_summaryWrite caller-supplied executive summary text directly to the project's active version. Omit, or pass null/empty/whitespace, to clear.Free
get_executive_summaryRead the executive summary text stored on a project version. Defaults to the active version; pass version_id to read a specific one.Free
generate_metadataAI-generate connection ports for a product5
generate_estimateGenerate a project estimateFree
generate_invoiceGenerate a project invoiceFree

Knowledge Base

ToolDescription
list_knowledge_base_manufacturersList available manufacturer documentation
search_knowledge_baseSearch manufacturer docs for wiring rules, specs, guides
get_knowledge_base_entryGet full article content

Ops Intelligence

ToolDescription
list_ops_projectsList active projects with risk scores and status
get_project_ops_statusGet ops data from linked Teams chats

Memory

ToolDescription
memory_readRead a stored preference or rule (checks your private memories, then company-shared)
memory_writeSave a preference or rule — private to you, or shared with your whole company
memory_appendAdd to an existing memory (follows the key: private first, then shared)
memory_listList all saved memory keys, tagged private or shared
memory_deleteDelete a memory — deleting a shared memory must say scope 'shared' explicitly

Skills

Skills are saved step-by-step procedures agents load and follow — memory stores facts, skills store how work gets done. Each skill carries a trigger description saying when to use it; when a request matches, the agent reads the skill and follows its steps. Skills are yours (scope private) or your company's (scope shared), and you can browse or edit them under Skills in the app.

ToolDescription
skill_listList saved procedures — yours and your company's — with the situations each one handles
skill_readRead a skill's full procedure by key (checks your private skills, then company-shared; pass scope to pin one)
skill_saveCreate or update a skill. Replacing an existing company skill requires overwrite so a team standard is never rewritten by accident
skill_deleteDelete a skill — deleting a company skill must say scope 'shared' explicitly

External Record Store

Five tools for storing and retrieving opaque JSON records under tenant-scoped namespaces. For an overview of what it does and how to set it up, see External Record Store. For parameter tables and example responses, see the External Record Store API. Manage the namespace registry under Global Project Configuration → External Records.

ToolDescription
external_records_putUpsert a record
external_records_getFetch by id
external_records_queryList updated since a watermark
external_records_deleteHard delete by id
external_records_list_namespacesDiscover valid namespaces