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.
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.
- Open Claude Desktop and go to Settings
- Navigate to Integrations
- Click Add and paste:
https://avstackr.com/mcp - Complete the OAuth login — you'll be redirected to AVstackr to authorize
- 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.
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
- Call
list_knowledge_base_manufacturersto see available knowledge bases - Call
search_knowledge_basewith a natural language question and optional manufacturer filter - Call
get_knowledge_base_entrywith 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: trueto 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 alternatesTILE ntext 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
| Tool | Description |
|---|---|
list_projects | List or search projects by name, job number, or customer |
get_project | Get project details with rooms and equipment counts |
create_project | Create a new project with optional customer |
update_project | Update 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_project | Delete a project permanently |
duplicate_project | Copy a project with all rooms and equipment |
get_project_financials | Get project financials (equipment, services, tax, total) |
get_scope_notes | Get scope call notes and timeline notes |
get_equipment_summary | Get bill of materials across all rooms |
get_schematic | Get the project's signal-flow schematic as a shareable viewer link |
get_schematic_image | Render 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_url | Mint a 7-day signed URL to download the project's Excel workbook (.xlsx) |
list_versions | List all versions for a project (Draft / Contracted / Archived) |
get_version | Get a single version by ID |
branch_version | Create a new draft version branched from an existing version |
activate_version | Activate an existing Draft or Contracted version. Fails on Archived — use promote_version instead |
promote_version | Bring an Archived version back as a new Draft and auto-activate it |
get_contract | Get the contract on a version (frozen state at approval) |
get_projection | Get the projected state of a version (Contract + all approved Change Orders applied) |
list_estimates_for_project | List all saved estimates for a project |
get_estimate | Get a saved estimate by ID with a fresh signed view URL |
approve_estimate | Approve 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_estimate | Undo 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_estimate | Delete a saved estimate by ID. Blocked (estimate_approved) if approved — void the approval first. |
list_invoices_for_project | List all saved invoices for a project |
get_invoice | Get a saved invoice by ID with a fresh signed view URL |
void_invoice | Void a saved invoice by ID — rolls back its billing allocations so it can be deleted. Idempotent. Optional reason for the audit trail. |
delete_invoice | Delete a saved invoice by ID. Blocked (invoice_has_live_allocations) if it has live allocations — void it first. |
Rooms & Equipment
| Tool | Description |
|---|---|
list_rooms | List all rooms in a project |
add_room | Add a room to a project |
update_room | Rename a room |
delete_room | Delete a room and all its equipment |
get_room_equipment | List equipment in a specific room |
add_line_item | Add a product to a room |
update_line_item | Full 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_item | Remove equipment from a room |
replace_room_equipment | Push 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_equipment | The 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_equipment | Duplicate 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_divider | Add 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_divider | List a room's category sections (the groupings shown as category in get_room_equipment) and their order. |
update_divider | Rename 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_divider | Remove a category section. The equipment under it stays — those items regroup under the section above. |
move_section | Move 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_room | Reorder 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_types | List 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
| Tool | Description |
|---|---|
search_products | Search 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_product | Get product details including connection metadata |
create_product | Add a new product to the catalog |
update_product | Update product details (make, model, price, etc.) |
update_product_metadata | Replace 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_lookup | Look up pricing by model or part number |
list_port_types | List 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.
| Tool | Description |
|---|---|
list_custom_blocks | List 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_block | Import 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_image | Return one block's processed artwork as a PNG alongside its details. Archived blocks still render |
rename_custom_block | Rename a block. Drawings already using it keep the same artwork — only the library label changes |
set_custom_block_archived | Retire 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_block | Pin 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
| Tool | Description |
|---|---|
list_customers | List all customers |
get_customer | Get customer details |
create_customer | Create a new customer |
update_customer | Update customer company, contact, email, phone, fax, website, billing/shipping addresses, payment terms, and notes |
delete_customer | Delete a customer |
AI Features
| Tool | Description | Credits |
|---|---|---|
ai_semantic_search | Natural language product search using embeddings | Free |
ai_match_equipment | Match an equipment list against the catalog | Free |
ai_parse_scope | Parse scope notes into product matches | Free |
ai_executive_summary | Generate 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_summary | Write caller-supplied executive summary text directly to the project's active version. Omit, or pass null/empty/whitespace, to clear. | Free |
get_executive_summary | Read the executive summary text stored on a project version. Defaults to the active version; pass version_id to read a specific one. | Free |
generate_metadata | AI-generate connection ports for a product | 5 |
generate_estimate | Generate a project estimate | Free |
generate_invoice | Generate a project invoice | Free |
Knowledge Base
| Tool | Description |
|---|---|
list_knowledge_base_manufacturers | List available manufacturer documentation |
search_knowledge_base | Search manufacturer docs for wiring rules, specs, guides |
get_knowledge_base_entry | Get full article content |
Ops Intelligence
| Tool | Description |
|---|---|
list_ops_projects | List active projects with risk scores and status |
get_project_ops_status | Get ops data from linked Teams chats |
Memory
| Tool | Description |
|---|---|
memory_read | Read a stored preference or rule (checks your private memories, then company-shared) |
memory_write | Save a preference or rule — private to you, or shared with your whole company |
memory_append | Add to an existing memory (follows the key: private first, then shared) |
memory_list | List all saved memory keys, tagged private or shared |
memory_delete | Delete 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.
| Tool | Description |
|---|---|
skill_list | List saved procedures — yours and your company's — with the situations each one handles |
skill_read | Read a skill's full procedure by key (checks your private skills, then company-shared; pass scope to pin one) |
skill_save | Create or update a skill. Replacing an existing company skill requires overwrite so a team standard is never rewritten by accident |
skill_delete | Delete 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.
| Tool | Description |
|---|---|
external_records_put | Upsert a record |
external_records_get | Fetch by id |
external_records_query | List updated since a watermark |
external_records_delete | Hard delete by id |
external_records_list_namespaces | Discover valid namespaces |
