MCP Server
AVstackr exposes 319 tools via the Model Context Protocol — manage projects, products, customers, rooms, equipment, schematics, AI features, and external record storage from an MCP client that signs in with OAuth, such as Claude Desktop or claude.ai.
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. 319 tools covering the full project lifecycle are available to a client that signs in with OAuth. A client that connects with an API key reaches the two knowledge base tools.
Requirements
| Requirement | Details |
|---|---|
| Endpoint | https://avstackr.com/mcp for every tool, or https://avstackr.com/mcp/kb for the knowledge base tools |
| Transport | Streamable HTTP |
| Authentication | OAuth 2.1 (Claude Desktop) on https://avstackr.com/mcp, or an API key (avs_...) on https://avstackr.com/mcp/kb |
| Plan | For the OAuth login on https://avstackr.com/mcp: an active plan or the free 30-day trial. For an API key on https://avstackr.com/mcp/kb: a paid plan. |
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 319 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. An API key connects to https://avstackr.com/mcp/kb. This endpoint offers the two knowledge base tools, search_knowledge_base and list_knowledge_base_manufacturers. The full tool set needs the OAuth login on https://avstackr.com/mcp, described above.
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/kb, the knowledge base endpoint, 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. Either way, an API key reaches the two knowledge base tools and not the rest of the tool set.
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/kb",
"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. The two knowledge base tools, search_knowledge_base and list_knowledge_base_manufacturers, 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 |
| Connection fails although the key is valid | Your account has no subscription that is active, trialing or past due |
| No tools showing | Check that the bridge script path is correct and Node.js is installed, and that MCP_URL is https://avstackr.com/mcp/kb |
Available Tools
Once connected, your MCP client will see the following tools:
TOOL search_knowledge_base
Search one manufacturer's documentation for a question. The tool returns the passages that match, best match first, each with its article id, title, source URL and text. Each article's text is divided into passages of at most 400 words. A short article is a single passage, so the search returns its whole text.
A search costs 1 credit. A search is charged whenever passages come back, and it has no relevance cut-off, so a search of a manufacturer that has documentation returns passages and is charged nearly every time. A search that returns no passage, names an unknown manufacturer or fails is not charged. The credit balance is checked before the search runs, and a team member with read-only access is refused before any search.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | The question, in plain language |
manufacturer |
string | Yes | Manufacturer slug from list_knowledge_base_manufacturers (e.g. biamp, crestron) |
limit |
integer | No | Maximum number of passages, 1 to 10 (default: 5). A value outside that range is moved to the nearest end of it |
The passages of one response hold at most 3,000 words in total, so a response can hold fewer passages than limit.
Example Response
{
"count": 1,
"credits_charged": 1,
"results": [
{
"article_id": 42,
"title": "TesiraFORTE Network Configuration",
"manufacturer_name": "Biamp",
"manufacturer_slug": "biamp",
"product_line": "tesira",
"source_url": "https://support.biamp.com/...",
"text": "The TesiraFORTE connects via Dante...",
"score": 0.891,
"match": "hybrid"
}
]
}
score is the reranker's relevance score for the passage, rounded to three decimal places, and null when the reranker did not score it. match says how the search found the passage: semantic (by meaning), keyword (by matching words) or hybrid (both). product_line and source_url are null when the article has none. A search that finds no passage returns "count": 0, "credits_charged": 0 and an empty results array.
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 and their slugs - Call
search_knowledge_basewith a natural language question and the manufacturer's slug. The passages it returns are the documentation text to answer from
Error Responses
| Scenario | Error |
|---|---|
| No Bearer token, or a Bearer token that fails authentication | Connection rejected — send an OAuth token to https://avstackr.com/mcp, or an API key to https://avstackr.com/mcp/kb |
An API key (avs_...) is sent to https://avstackr.com/mcp |
Connection rejected — an API key connects to https://avstackr.com/mcp/kb |
| An OAuth token for a company with no active plan or free trial | Connection rejected — a plan or a live free trial is required |
| An API key for a company with no paid plan | Connection rejected — a paid plan is required |
search_knowledge_base: query is blank |
{"error": "query is required"} |
search_knowledge_base: manufacturer is blank |
{"error": "manufacturer is required. list_knowledge_base_manufacturers returns the slugs."} |
search_knowledge_base: no active manufacturer has the slug |
{"error": "Unknown manufacturer 'nosuch'. list_knowledge_base_manufacturers returns the slugs.", "credits_charged": 0} |
search_knowledge_base: the team member has read-only access |
{"error": "You don't have permission to run paid knowledge base searches. Contact your administrator."} |
search_knowledge_base: the organization does not have the credit for one search |
{"error": "Insufficient credits for knowledge base search", "credits_required": 1} |
search_knowledge_base: the search could not be completed |
{"error": "Knowledge base search failed. Nothing was charged."} |
None of the search_knowledge_base errors above is charged.
The tools below need the OAuth login on https://avstackr.com/mcp.
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. The OAuth login on https://avstackr.com/mcp reaches every tool and needs an active plan or a live free trial. An API key on https://avstackr.com/mcp/kb reaches the two tools in the Knowledge Base table and needs a paid plan.
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. Optional quantity (a whole number of 1 or more; 1 when omitted) sets how many identical copies of the room the project has. Optional include_travel and travel_amount set the room's travel cost: an amount alone turns travel on, the amount must be 0 or more, and include_travel: false with an amount is refused. |
update_room | Rename a room, set its quantity, per-room freight or travel, or change whether it is optional. Only the fields you send change. quantity: a whole number of 1 or more. include_travel: true adds travel cost to the room; false removes it and clears the amount. travel_amount: the travel cost in dollars, 0 or more; sending it alone turns travel on, and sending it with include_travel: false is refused. A refused quantity or travel value changes nothing. is_add_alt: true marks the room optional (an add alternate); false makes it a base room and clears any replaces link. replaces_room_id: the base room this optional room replaces on the proposal; 0 clears the link, and omitting it leaves the link unchanged. A refused link returns room_link_refused with the reason, and nothing is changed. |
duplicate_room_as_alternate | Create an optional copy of a room that REPLACES that room on the proposal, priced as the difference (Deduct / Add / No change). The copy is named "<room> (Alt)" ("(Alt 2)" and so on when that name is taken) and carries the room's figures, equipment and sections. Not for adding identical rooms: use room quantity for that. The room must be a base room of the project, not optional and not archived; a refusal returns room_link_refused with the reason. |
delete_room | Delete a room and all its equipment |
get_room_equipment | List equipment in a specific room. Each line carries labor: its hours per labor type, per unit — the complete set update_line_item's labor_json replaces. Each item also carries cad_system, the line's system ({ id, name, abbreviation }), or null when unassigned. |
add_line_item | Add a product to a room. The line starts with the product's default hours per labor type, when the product has any. Optional cad_system (a system id or abbreviation from list_cad_systems) puts the new line in that system. Optional order sets the new line's position in the room's list, which items and dividers share; it is a raw value, so nothing shifts to make room, and omitting it appends the line after everything in the room. |
update_line_item | Full per-line editor: quantity, cost, override %, labor/programming/configuration hours, hours per labor type via labor_json, equipment source, notes, snapshot overrides (make/model/part/description), and cable assignments via cables_json. Sparse — only supplied fields change. labor_json is replace-all: the array becomes the line's complete set of labor types ('[]' clears them), and an entry for a built-in type wins over the matching hours field sent in the same call. Set clear_override_percent: true to reset markup to the tenant default. cad_system sets the line's system (an id or abbreviation from list_cad_systems); an empty string unassigns it. order sets the line's position in the room's list, shared with the dividers, as a raw value: nothing shifts to make room, and omitting it leaves the position unchanged. |
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 and cable-schedule rows, 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 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 renumbers the room: every item and section at or below the target moves 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. To organize an existing room into sections, call replace_room_equipment with sections_json instead of looping this tool. 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 renumbers the room: every item and section at or below the target moves 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 section: 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. To organize an existing room into sections, call replace_room_equipment with sections_json instead of looping this tool. 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). Each type also carries cable_category_id, the cable category it belongs to, such as network; product_id, the catalog product the cable is bought as; and part_number, that product's part number, or its make and model when the product has no part number. A type with no category has null for cable_category_id, and a type with no product has null for product_id and an empty part_number. Use this to find cable_type_id values for update_line_item's cables_json. Pass include_inactive=true to also see deactivated types. |
list_labor_types | List active labor types in the tenant's catalog — the built-in Install, Programming and Configuration plus any the company added, with each type's default cost and sell rate per hour and whether its hours count toward the project-management base. Use this to find labor_type_id values for update_line_item's labor_json and the replace tools' labor entries. Pass include_inactive=true to also see retired types. |
Floor Plans
| Tool | Description |
|---|---|
list_floorplans | List a project's floor plans, the plans the Measure page shows, in their order. Each carries its id, name, sort_order and room_count (the rooms linked to it, archived rooms included). |
create_floorplan | Create a floor plan in a project, after the existing ones. name is 1 to 200 characters after trimming, and two floor plans may share a name. The project's first floor plan takes over every room of the project, archived rooms and the rooms of other versions included; linked_room_count in the reply says how many. A later floor plan starts with no rooms. |
rename_floorplan | Rename a floor plan by floorplan_id. name is 1 to 200 characters after trimming. Only the name changes. An unknown floor plan returns Floor plan not found. |
delete_floorplan | Delete a floor plan by floorplan_id. Its rooms are kept and unlinked; unlinked_room_count in the reply says how many. Any sheet viewport that pointed at the floor plan shows a placeholder. This cannot be undone. |
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. Each row also carries variant_group_id, variant_group_name, variant_option and variant_label. They describe the product's variant group and are null when the product is in none. |
get_product | Get product details including connection metadata. cad_category is the product's device category ({ id, name, abbreviation }), or null when it is unclassified |
create_product | Add a new product to the catalog. Optional cad_category (a category id or abbreviation from list_cad_categories) classifies it; left out, the product is unclassified |
update_product | Update product details (make, model, price, etc.). cad_category sets the device category (an id or abbreviation); an empty string clears it |
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. Pass apply_to_variant_ids to copy the saved metadata onto other members of the product's variant group. It is the same copy apply_metadata_to_variants makes. For a voice agent, it needs that tool's permission. include_block_skin (default true) also copies the product's block skin when it has one. The copy is reported under applied_to_variants. When the product is in no variant group, the save still succeeds and apply_error is not_grouped. |
price_lookup | Look up pricing by model or part number |
price_check_room | Price-check every catalog line in a room and write the cost found onto each line that has no cost, a negative cost, or a 0.00 cost nobody accepted. A line with a cost above 0.00, or an accepted 0.00, is skipped unless force_update is true. Without vendor_id the price comes from the catalog; with it, from that vendor's pricing API. The price is converted to the project's currency at the project's exchange rate before it is written. A room on a contracted version answers LOCKED_VERSION and nothing is written. |
list_port_types | List valid port types for connection metadata |
list_variant_groups | List variant groups. A variant group is one device sold in several finishes, and each member is a catalog product with its own label. search matches the group name or the make. limit defaults to 50 and is capped at 200. Each group reports its make, option name, member count and the members' labels. |
get_variant_group | Get one variant group by group_id with its members: product id, model, part, label, catalog price, vendor, product type and whether the product has connection metadata. |
create_variant_group | Create a variant group from two or more catalog products of one make that are in no group yet. Takes name, option_name and an optional description. product_ids and variant_labels are parallel lists: the label at each position belongs to the product id at the same position. A broken grouping rule returns the error variant_group_rule with every problem listed, and nothing is written. |
update_variant_group | Rename a variant group, change its option_name, or change its description. A field that is left out keeps its value. An empty description clears it. |
delete_variant_group | Delete a variant group and ungroup its products. The products stay in the catalog and lose their group and label. For a voice agent this tool is off until an administrator switches it on under Delete Operations in the agent's permissions. |
add_products_to_variant_group | Add catalog products to a variant group. The products must be of the group's make and in no group yet. product_ids and variant_labels are parallel lists, as in create_variant_group. |
relabel_variant_member | Change one member's label within its variant group. Takes group_id, product_id and variant_label. |
remove_product_from_variant_group | Take one product out of its variant group and clear its label. The product stays in the catalog. Removing the last member deletes the group. |
apply_metadata_to_variants | Copy the saved connection metadata of source_product_id onto target_product_ids, overwriting theirs. The targets must be other members of the source's variant group. Any that are not come back under skipped_product_ids and are left untouched. include_block_skin (default true) also copies the source's block skin when it has one. Devices already placed on drawings keep their own copy of the metadata. |
Device Tags
Every physical unit on a project can carry a device tag such as DISP-003, built from the company's tag pattern out of the product's device category and the line's system. See Device Tags. Every tool below that changes data returns READ_ONLY_ACCESS for a Read-only user in Claude Desktop and in AVIP Assistant, and saves nothing. A voice agent linked to a Read-only user refuses earlier, with its "not enabled for this agent" message, and also saves nothing. AVIP Assistant, the in-app copilot, has the same tools with the same arguments.
| Tool | Description |
|---|---|
list_cad_categories | List the device categories products are classified by: id, name, the abbreviation used in tags, is_drawable, expected_on (the views its units are expected on, empty when the category is not checked) and order. The first read creates the default list |
set_product_category | Set or clear a catalog product's device category. category is an id or an abbreviation (7 or DISP); empty or left out clears it. Returns cad_category. An unknown category returns CATEGORY_NOT_FOUND |
add_cad_category | Add a device category at the end of the list. name and abbreviation (1 to 8 letters or digits, stored upper-case) are required; is_drawable defaults to true, and expected_on lists the views its units are expected on, none when left out. A company with no categories yet gets the default list first. A name or abbreviation already used returns DUPLICATE_CATEGORY and saves nothing. Returns the category as list_cad_categories lists it |
update_cad_category | Change a category's name, abbreviation, is_drawable or expected_on (the whole new list; an empty list clears it). category is an id or an abbreviation. A field left out keeps its value, and a call with none returns INVALID_INPUT. Tags already given keep their text. A name or abbreviation another category uses returns DUPLICATE_CATEGORY, and a category someone else changed during the call returns ROW_CHANGED; either way nothing is saved. Returns the category as list_cad_categories lists it |
set_cad_category_block | Set or clear a category's default block for one view; its other views stay. category is an id or an abbreviation; block is an id or a display name from list_cad_blocks, and empty or left out clears the view. visibility_state picks a dynamic block's state and needs a block, or returns INVALID_INPUT. A product of the category with no block of its own for the view is drawn with this default. Returns category_id and the category's cad_blocks. A refused block returns VIEW_MISMATCH, BLOCK_ARCHIVED or INVALID_VISIBILITY_STATE |
list_cad_systems | List the systems equipment lines are assigned to: id, name, the abbreviation used in tags and order. The first read creates the default list |
set_line_item_system | Assign many lines to a system, or unassign them. equipment_ids are line item ids from get_room_equipment; system is an id or an abbreviation, and empty or left out unassigns. Ids that are not the company's lines are skipped; updated counts the lines set. An unknown system returns CAD_SYSTEM_NOT_FOUND |
add_cad_system | Add a system at the end of the list, with name and abbreviation (1 to 8 letters or digits, stored upper-case). A company with no systems yet gets the default list first. A name or abbreviation already used returns DUPLICATE_SYSTEM and saves nothing. Returns the system as list_cad_systems lists it |
update_cad_system | Change a system's name or abbreviation. system is an id or an abbreviation. A field left out keeps its value, and a call with neither returns INVALID_INPUT. Tags already given keep their text. A name or abbreviation another system uses returns DUPLICATE_SYSTEM, and a system someone else changed during the call returns ROW_CHANGED; either way nothing is saved. Returns the system as list_cad_systems lists it |
list_device_units | List a line's physical units with each unit's instance_id, instance_key (its permanent id), unit_number, device_tag (null while untagged) and attribute entries. Units are created on first read |
add_unit_attribute | Add one attribute entry, such as a serial number, to one unit. The unit is named by instance_id from list_device_units, or by equipment_id and unit_number (1-based), which creates the unit when it does not exist yet. The key is named by key_id or by the name of an active key in key_name, ignoring case; key_id wins. value may be empty, and a unit may hold several entries with the same key. Returns the new entry_id |
update_unit_attribute | Change one per-unit entry, by the entry_id from list_device_units. value left out keeps the value. key_id (any key) or key_name (active keys only) moves the entry to another key |
delete_unit_attribute | Delete one per-unit entry, by the entry_id from list_device_units. The unit's other entries stay. A new voice agent has this tool off until an administrator switches it on under Delete Operations in the agent's permissions |
set_device_tag | Set one unit's device tag by hand, by instance_id. A tag is 1 to 50 letters, digits and . _ / -, starts with a letter or digit, and is unique within the project; otherwise the result is INVALID_DEVICE_TAG or DEVICE_TAG_IN_USE with the reason |
assign_device_tags | Create the units of every line in a project's active version and tag each untagged unit. Existing tags never change. Returns units_seen and tags_assigned |
get_cad_units | Read a project's CAD units for one view (plan, ceiling, elevation or schematic): one row per physical unit of the active version's rooms with instance_key, device_tag, dimensions in inches, attributes keyed by upper-case CAD tag, the block resolved for the view (its source is product, category or generic), rack_placement and a fingerprint. project_id is required; room_id reads one room, and include_non_drawable adds units whose category is not drawable. Read-only: lines_without_units counts lines whose units assign_device_tags has not created yet |
list_cad_tags | List the CAD tag catalog, the attribute tags a block can carry for the plugin to fill. canonical holds the built-in tags, each with tag, description and hidden (a tag the block is expected to keep invisible); tenant holds the company's attribute keys that map to a CAD tag, each with tag, key_id, key_name and is_active. Only active keys are filled into drawings. Takes no arguments |
list_cad_blocks | List the CAD block library by name: each block's id, name, block_name, view_types, visibility_states, supported_tags and unmapped_tags, its file, and usage per view. view keeps the blocks that suit it, and include_archived adds archived ones. Blocks are uploaded from the AutoCAD plugin, not through MCP |
set_product_cad_block | Set or clear the block a product uses for one view; its other views stay. product_id is the catalog product; block is an id or a display name from list_cad_blocks, and empty or left out clears the view, which then uses the category's default block or a generic box. visibility_state picks a dynamic block's state. Returns the product's cad_blocks. A refused block returns VIEW_MISMATCH, BLOCK_ARCHIVED or INVALID_VISIBILITY_STATE |
update_cad_block | Change a block's display name, description (an empty string clears it) or view_types (the whole new list), by block_id. The file itself is replaced only from the AutoCAD plugin. A field left out keeps its value, and a call with none returns INVALID_INPUT. Removing a view that products or categories use the block for returns VIEW_IN_USE, a name another live block has returns NAME_IN_USE, and an unknown block returns BLOCK_NOT_FOUND; either way nothing is saved. Returns the block as list_cad_blocks lists it |
set_cad_block_archived | Archive or unarchive a block, by block_id, with archived true or false. An archived block keeps its file and assignments, but no product or category can be given it, and units fall back to the category's default block, then to a generic box, until it is unarchived. Drawings that already hold it are untouched. Unarchiving a block whose name a live block now has returns NAME_IN_USE. Returns block_id and is_archived |
list_cad_drawings | List the AutoCAD drawings linked to a project, in the order they were linked: each drawing's id, name, view, fingerprint_guid, file_path_hint, placed_units, last_submit_at, last_apply_at and created_at. project_id is required. Drawings are linked from the AutoCAD plugin, not through MCP |
rename_cad_drawing | Rename a drawing linked to a project, by project_id and drawing_id. name is 1 to 200 characters after trimming; a name outside that returns INVALID_INPUT. The DWG file and its placements are untouched. Returns the drawing as list_cad_drawings lists it; an unknown drawing returns DRAWING_NOT_FOUND |
list_cad_placements | List the units placed on a project's drawings, one row per unit per drawing: placement_id, drawing_id, drawing_name, view, the unit's instance_id, instance_key, device_tag and unit_number, its make, model and room, the block's handle, block_name, cad_block_id, layer and visibility_state, last_seen_at and last_pushed_fingerprint. project_id is required; drawing_id lists one drawing's |
unplace_cad_unit | Remove one placement, by project_id and the placement_id from list_cad_placements. The unit stays in the project and its block stays in the DWG. Returns placement_id, drawing_id and instance_id; an unknown placement returns PLACEMENT_NOT_FOUND. Drawings are synced from the plugin: there is no submit or apply tool |
remove_cad_drawing | Remove a drawing's link to a project, in two calls. A call with project_id and drawing_id alone changes nothing and returns a preview: drawing_id, name, view, placements and confirm_required. The preview is shown to the user, and the confirming call sends the preview's placements as confirm_placements; a count that changed in between gets a fresh preview. The confirming call removes the link and its placements and returns drawing_id, name and removed_placements. The units stay in the project, and only the AutoCAD plugin can link the drawing again. For a voice agent this tool is off until an administrator switches it on under Delete Operations in the agent's permissions |
get_not_drawn_units | Read the units not drawn on a project's active version: units whose category expects them on a view the project has a drawing of, while no drawing of that view places them. project_id is required; room_id reads one room. Returns project_id, version_id, version_name, open_count and rooms, each with room_id, room_name, is_archived, its own open_count and its lines, each with equipment_id, product and gaps (instance_id, device_tag, view, accepted). open_count counts the units with a gap not accepted, as the room's Needs attention tile does. A room the active version does not have returns ROOM_NOT_FOUND |
accept_not_drawn_units | Accept gaps, or take acceptances back, on a project's active version. project_id is required; accept and withdraw each list instance_id and view pairs from get_not_drawn_units, and a call with neither returns INVALID_INPUT. A pair that is not an open gap to accept or an accepted gap to withdraw comes back in skipped with its reason. A unit the project does not have returns UNIT_NOT_FOUND, a unit with no room in the active version UNIT_NOT_IN_ACTIVE_VERSION, and a contracted version LOCKED_VERSION; then nothing is saved. Returns version_id, version_name, accepted, withdrawn, skipped and rooms, each with room_id, saved and open_count |
The line item attribute key tools carry each key's CAD attribute tag, the AutoCAD block attribute it maps to: list_line_item_attribute_keys returns cad_attribute_tag on each key, and create_line_item_attribute_key and rename_line_item_attribute_key take an optional cad_attribute_tag (on rename, an empty string clears it). They carry how the key syncs with drawings too: each key returns cad_sync, which is push, pull, both, or none for a key without a tag, and the create and rename tools take an optional cad_sync of push, pull or both, set after the tag. A key given a tag without one syncs push. Any other word returns INVALID_INPUT before anything is saved; cad_sync on a key left without a tag returns INVALID_SYNC_DIRECTION, and create then does not keep the key while rename keeps the new name and tag.
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 |
Vendors
| Tool | Description |
|---|---|
list_vendors | List all vendors with name, contact person, email, phone, city, whether the vendor is active, and whether its API integration is on. |
get_vendor | Get a vendor's details: contact person, address, phone, fax, email, website, notes, whether it is active, and whether its API integration is on. |
create_vendor | Create a new vendor. Only name is required. |
update_vendor | Update a vendor's name, contact person, address, phone, fax, email, website, notes, or whether it is active. Only the fields you send change. |
delete_vendor | Delete a vendor. |
get_vendor_config | Get a vendor's API integration status: the endpoint, pricing, availability and authentication URLs, whether the integration is on, the OAuth scope, when the cached token expires, and the time, result and error of the last connection test. Each credential is answered only as whether it is set (has_client_credentials, has_bearer_token, has_api_key, has_subscription_key). No credential value is ever returned. |
test_vendor_config | Test a vendor's API connection. Returns success and a message that says what was verified or why the test failed. A test that completes, a failed subscription key check and an unexpected error are stored as the vendor's last test. A test that stops earlier is not stored, and the previous result stays: this happens when the integration is off, when no authentication URL is set, or when authentication fails. |
toggle_vendor_config | Switch a vendor's API integration on or off with enabled. A vendor whose integration is off is not called for pricing or availability. |
delete_vendor_config | Clear a vendor's API integration: its URLs, credentials, cached token and last test result are removed, and the integration is switched off. The vendor itself is kept. To connect it again, enter the addresses and credentials again. |
No tool sets a vendor's API addresses or credentials. Enter them with Configure API on the Vendors page. A ReadOnly user can read a vendor's API integration status but cannot test, switch or clear it.
Configuration
| Tool | Description |
|---|---|
get_global_config | Get your company's global project configuration, the defaults new projects start from: labor, programming and configuration rates, equipment markup, the drawings, project management, shipping and tax percentages, the shipping mode, cost rates, and the Cat6A cable amounts and hours. A value nobody has set is null. shipping_mode is Percentage, FlatProject or FlatPerRoom. The last field, cable_numbers, is your company's cable number scheme: pattern, the pattern written as text, such as {Type}{Seq}; seq_digits, the number of digits Sequence shows; and series, one entry for each of the eleven cable categories, with its prefix (the registry letter), name, code, start_number and default_product_id, the id of the product from your catalog that is the category's default product, or null when none is set. |
update_global_config | Change the global project configuration. config_json is a JSON object of the fields to change, named as get_global_config returns them (for example labor_rate or equipment_markup). A field left out, or sent as null, keeps its value. The whole output of get_global_config is accepted as it is: created_at and updated_at are ignored. An unknown field changes nothing, and the error lists the valid fields in valid_fields. cable_numbers changes the cable number scheme. It takes pattern, seq_digits and series, and a part left out keeps its value. Inside cable_numbers, a pattern sent as null restores the default pattern, while seq_digits, code and start_number sent as null count as left out. In series, a category's entry sets its code, its start_number, its default_product_id or any of them. default_product_id is the id of a product in your catalog; 0 clears the default product, and null or left out keeps it. An entry sent as null returns the category's code and start number to the registry defaults, its registry letter and a start number of 100, and the default product stays. prefix and name are read-only and ignored. If the pattern, the digits, a code, a start number or a category is not valid, or a default product is not a product in your catalog, nothing in the call changes, and the error says what is wrong. Returns the configuration as stored. A ReadOnly user can read the configuration but not change it. |
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 |
suggest_equipment | Suggest catalog products for a room type, ranked by relevance across the equipment categories that room type needs | charged per category search (at least 1 credit each; 2–8 searches per call); needs a balance of at least 1 credit |
generate_product_description | AI-generate a product's part number, short description and long description from its make and, optionally, its model and part number, grounded in the manufacturer's documentation | charged per generated description |
generate_estimate | Generate a project estimate | Free |
generate_invoice | Generate a project invoice | Free |
Knowledge Base
| Tool | Description | Credits |
|---|---|---|
list_knowledge_base_manufacturers | List available manufacturer documentation | Free |
search_knowledge_base | Search one manufacturer's documentation for wiring rules, specs and guides. Returns the matching passages | 1 when passages are returned |
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), your company's (scope shared), or the platform's (scope platform, maintained by AVstackr and read-only), and you can browse or edit them under Skills in the app.
| Tool | Description |
|---|---|
skill_list | List saved procedures — yours, your company's, and the platform's — with the situations each one handles. Each entry also carries requires_license (the license a platform procedure is sold under, or null) and licensed (true when your company holds it or none is required) |
skill_read | Read a skill's full procedure by key (checks private, then company, then platform; pass scope to pin one). A licensed platform procedure your company hasn't bought returns license_required with the purchase page in its message, instead of a body |
skill_save | Create or update a skill. Replacing an existing company skill requires overwrite so a team standard is never rewritten by accident. Scope platform is refused; save under shared with the same key to override a platform skill for your company |
skill_delete | Delete a skill — deleting a company skill must say scope 'shared' explicitly. Platform skills are not deletable through MCP |
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 |
