Docs/Guides/Developer Platform

    Building Custom MCP Servers

    Last updated · MAR 2026·Read as Markdown

    Connect your control system code, programming libraries, manufacturer documentation, ticketing systems, and field-service APIs to AVCodex agents.

    A custom MCP server lets you wire any AV data source or internal API into your AVCodex-powered agents. This guide walks through protocol basics, two production patterns most AV teams need (REST API adapters and database servers), and the security and deployment steps to ship it.

    The Model Context Protocol (MCP) is an open standard from Anthropic that defines how AI systems talk to external tools and data sources. Treat it as a universal adapter between your AVCodex agent and the systems your AV ops team already runs: ticketing, asset management, programming repos, room-monitoring dashboards, manufacturer APIs.

    Why it matters for AV:

    • Standardized interface: One protocol for Crestron XiO, Q-SYS Reflect, ServiceNow, your own Postgres job database, and anything else.
    • AI-optimized: Designed for how LLMs discover and call tools. The Alchemist and your deployed agents pick the right one without hand-holding.
    • Secure: Patterns for authentication and authorization are baked in.
    • Flexible: Works with REST APIs, SQL databases, file stores, and message buses.

    MCP uses JSON-RPC 2.0 for communication. Two core methods carry most of the weight:

    tools/list#

    Returns the tools an AVCodex agent can call.

    tools/call#

    Executes a tool with the supplied arguments.

    The official MCP SDKs handle protocol details so you can focus on AV-specific business logic. This guide uses the TypeScript SDK because it has the most mature surface area and the best documentation. The Python SDK is a fine alternative if your control-system tooling already lives in Python.

    Build a minimal server to see the moving parts before you wire it to anything real.

    Project Setup#

    bash
    mkdir avcodex-mcp-server
    cd avcodex-mcp-server
    npm init -y
    npm install @modelcontextprotocol/sdk express zod
    npm install -D typescript @types/node @types/express ts-node

    Minimal Server Code#

    typescript
    // src/server.ts
    import express from "express";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import { z } from "zod";
    import { randomUUID } from "crypto";
    
    const app = express();
    app.use(express.json());
    
    // Create MCP server
    const server = new McpServer({
      name: "avcodex-room-status",
      version: "1.0.0"
    });
    
    // Register a simple tool
    server.tool(
      "room_status",
      "Return a one-line status string for a conference room",
      {
        roomId: z.string().describe("Room ID, e.g. 'BOS-14-Boardroom'")
      },
      async ({ roomId }) => {
        return {
          content: [{ type: "text", text: `Room ${roomId}: online, displays awake, DSP healthy.` }]
        };
      }
    );
    
    // HTTP transport setup
    const transports: Record<string, StreamableHTTPServerTransport> = {};
    
    app.post("/mcp", async (req, res) => {
      const sessionId = req.headers["mcp-session-id"] as string;
    
      let transport = transports[sessionId];
      if (!transport) {
        transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: () => randomUUID()
        });
        await server.connect(transport);
      }
    
      const newSessionId = (transport as any)._sessionId;
      if (newSessionId) transports[newSessionId] = transport;
    
      await transport.handleRequest(req, res, req.body);
    });
    
    app.get("/mcp", async (req, res) => {
      const sessionId = req.headers["mcp-session-id"] as string;
      const transport = transports[sessionId];
      if (transport) await transport.handleRequest(req, res);
      else res.status(404).json({ error: "Session not found" });
    });
    
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
      console.log(`MCP server running at http://localhost:${PORT}/mcp`);
    });

    Run it:

    bash
    npx ts-node src/server.ts

    You now have a working MCP server. Next, build something a programmer or field tech would actually use.


    The most common case for AV: wrapping the REST APIs you already maintain (ticketing, room asset DB, internal programmer tooling) as MCP tools.

    When to Use This Pattern#

    • You have existing REST or GraphQL APIs (ServiceNow, Jira, an internal commissioning tracker).
    • You want to expose internal services to your AVCodex agent without rewriting them.
    • You need to add AI on top of established back-ends without touching the back-ends.

    Complete Example: Site and Room Asset API#

    This example wraps a typical AV ops API: search rooms, look up a room, register a new room after a deployment, update commissioning status, pull recent service tickets.

    typescript
    // src/rest-adapter-server.ts
    import express from "express";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import { z } from "zod";
    import { randomUUID } from "crypto";
    
    const app = express();
    app.use(express.json());
    
    // Configuration from environment
    const API_BASE = process.env.API_BASE_URL || "https://api.avops.example.com";
    const API_KEY = process.env.API_KEY || "";
    
    // Helper for API calls
    async function apiCall(
      endpoint: string,
      options: RequestInit = {}
    ): Promise<{ ok: boolean; data?: any; error?: string }> {
      try {
        const response = await fetch(`${API_BASE}${endpoint}`, {
          ...options,
          headers: {
            "Authorization": `Bearer ${API_KEY}`,
            "Content-Type": "application/json",
            ...options.headers,
          },
        });
    
        if (!response.ok) {
          const errorText = await response.text();
          return { ok: false, error: `API error (${response.status}): ${errorText}` };
        }
    
        const data = await response.json();
        return { ok: true, data };
      } catch (err: any) {
        return { ok: false, error: `Network error: ${err.message}` };
      }
    }
    
    // Create MCP server
    const server = new McpServer({
      name: "av-site-asset-api",
      version: "1.0.0"
    });
    
    // Tool 1: Search Rooms
    server.tool(
      "search_rooms",
      "Search rooms by name, building, or room type. Returns matching room records.",
      {
        query: z.string().describe("Search query (matches room name, building, code)"),
        limit: z.number().optional().describe("Max results (default: 10, max: 100)"),
        offset: z.number().optional().describe("Pagination offset")
      },
      async ({ query, limit = 10, offset = 0 }) => {
        const safeLimit = Math.min(limit, 100);
        const result = await apiCall(
          `/rooms/search?q=${encodeURIComponent(query)}&limit=${safeLimit}&offset=${offset}`
        );
    
        if (!result.ok) {
          return {
            content: [{ type: "text", text: `Error: ${result.error}` }],
            isError: true
          };
        }
    
        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              rooms: result.data.rooms,
              total: result.data.total,
              hasMore: result.data.total > offset + safeLimit
            }, null, 2)
          }]
        };
      }
    );
    
    // Tool 2: Get Room by ID
    server.tool(
      "get_room",
      "Fetch the full record for a room: gear list, control system, commissioning notes.",
      {
        roomId: z.string().describe("Room ID, e.g. 'BOS-14-Boardroom'")
      },
      async ({ roomId }) => {
        const result = await apiCall(`/rooms/${encodeURIComponent(roomId)}`);
    
        if (!result.ok) {
          return {
            content: [{ type: "text", text: `Error: ${result.error}` }],
            isError: true
          };
        }
    
        return {
          content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
        };
      }
    );
    
    // Tool 3: Register Room
    server.tool(
      "register_room",
      "Register a newly commissioned room. Returns the created record with its ID.",
      {
        name: z.string().describe("Room name (e.g. '14F Boardroom')"),
        building: z.string().describe("Building code (e.g. 'BOS-HQ')"),
        roomType: z.enum(["conference", "classroom", "broadcast", "command-center", "huddle", "auditorium"]).describe("Room category"),
        controlSystem: z.string().optional().describe("Control system family (Crestron, Q-SYS, AMX, Extron)")
      },
      async ({ name, building, roomType, controlSystem }) => {
        const result = await apiCall("/rooms", {
          method: "POST",
          body: JSON.stringify({ name, building, roomType, controlSystem })
        });
    
        if (!result.ok) {
          return {
            content: [{ type: "text", text: `Error registering room: ${result.error}` }],
            isError: true
          };
        }
    
        return {
          content: [{
            type: "text",
            text: `Room registered:\n${JSON.stringify(result.data, null, 2)}`
          }]
        };
      }
    );
    
    // Tool 4: Update Room
    server.tool(
      "update_room",
      "Update a room record. Send only the fields you want to change.",
      {
        roomId: z.string().describe("Room ID to update"),
        name: z.string().optional().describe("New room name"),
        controlSystem: z.string().optional().describe("Updated control system family"),
        commissioningStatus: z.enum(["pending", "in-progress", "complete", "punch-list"]).optional().describe("Commissioning state"),
        notes: z.string().optional().describe("Free-form notes for the AS-built record"),
        active: z.boolean().optional().describe("Whether the room is in service")
      },
      async ({ roomId, ...updates }) => {
        // Filter out undefined values
        const cleanUpdates = Object.fromEntries(
          Object.entries(updates).filter(([_, v]) => v !== undefined)
        );
    
        if (Object.keys(cleanUpdates).length === 0) {
          return {
            content: [{ type: "text", text: "Error: No fields provided to update" }],
            isError: true
          };
        }
    
        const result = await apiCall(`/rooms/${encodeURIComponent(roomId)}`, {
          method: "PATCH",
          body: JSON.stringify(cleanUpdates)
        });
    
        if (!result.ok) {
          return {
            content: [{ type: "text", text: `Error updating room: ${result.error}` }],
            isError: true
          };
        }
    
        return {
          content: [{
            type: "text",
            text: `Room updated:\n${JSON.stringify(result.data, null, 2)}`
          }]
        };
      }
    );
    
    // Tool 5: List Recent Service Tickets for a Room
    server.tool(
      "list_room_tickets",
      "Get recent service tickets and incidents for a room.",
      {
        roomId: z.string().describe("Room ID"),
        days: z.number().optional().describe("Days of history (default: 7, max: 30)")
      },
      async ({ roomId, days = 7 }) => {
        const safeDays = Math.min(days, 30);
        const result = await apiCall(
          `/rooms/${encodeURIComponent(roomId)}/tickets?days=${safeDays}`
        );
    
        if (!result.ok) {
          return {
            content: [{ type: "text", text: `Error: ${result.error}` }],
            isError: true
          };
        }
    
        return {
          content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
        };
      }
    );
    
    // HTTP transport setup (same as before)
    const transports: Record<string, StreamableHTTPServerTransport> = {};
    
    app.post("/mcp", async (req, res) => {
      const sessionId = req.headers["mcp-session-id"] as string;
      let transport = transports[sessionId];
    
      if (!transport) {
        transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: () => randomUUID()
        });
        await server.connect(transport);
      }
    
      const newSessionId = (transport as any)._sessionId;
      if (newSessionId) transports[newSessionId] = transport;
    
      await transport.handleRequest(req, res, req.body);
    });
    
    app.get("/mcp", async (req, res) => {
      const sessionId = req.headers["mcp-session-id"] as string;
      const transport = transports[sessionId];
      if (transport) await transport.handleRequest(req, res);
      else res.status(404).json({ error: "Session not found" });
    });
    
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
      console.log(`AV asset API adapter running at http://localhost:${PORT}/mcp`);
    });

    Authentication Pass-Through#

    If your back-end requires per-tech credentials (a field engineer's own ServiceNow token, for example), pass them through:

    typescript
    // Extract auth from MCP request context
    server.tool(
      "get_my_open_tickets",
      "Get the calling tech's open service tickets",
      {},
      async (args, context) => {
        // The client can pass user-specific tokens in the request
        const userToken = context?.meta?.userToken as string;
    
        const result = await apiCall("/me/tickets", {
          headers: userToken ? { "Authorization": `Bearer ${userToken}` } : {}
        });
    
        // ... handle response
      }
    );

    Expose a database to your AVCodex agent for room intelligence, asset rollups, and on-demand reporting (think: "how many DM-NVX endpoints are deployed across the West region?").

    Warning: Never expose production databases directly to AI agents. Use a read replica, a sanitized copy, or a strictly read-only connection. The same rule that protects your headend protects your data.

    Complete Example: PostgreSQL Server for AV Asset Data#

    typescript
    // src/database-server.ts
    import express from "express";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import { z } from "zod";
    import { randomUUID } from "crypto";
    import { Pool } from "pg";
    
    const app = express();
    app.use(express.json());
    
    // Database connection (USE A READ REPLICA)
    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 10, // Connection pool size
    });
    
    // Create MCP server
    const server = new McpServer({
      name: "av-data-explorer",
      version: "1.0.0"
    });
    
    // Tool 1: List Tables
    server.tool(
      "list_tables",
      "List all tables in the AV asset database with column counts.",
      {
        schema: z.string().optional().describe("Schema name (default: public)")
      },
      async ({ schema = "public" }) => {
        try {
          const result = await pool.query(`
            SELECT
              table_name,
              (SELECT COUNT(*) FROM information_schema.columns c
               WHERE c.table_name = t.table_name AND c.table_schema = t.table_schema) as column_count
            FROM information_schema.tables t
            WHERE table_schema = $1 AND table_type = 'BASE TABLE'
            ORDER BY table_name
          `, [schema]);
    
          return {
            content: [{
              type: "text",
              text: JSON.stringify({
                schema,
                tables: result.rows,
                count: result.rows.length
              }, null, 2)
            }]
          };
        } catch (err: any) {
          return {
            content: [{ type: "text", text: `Database error: ${err.message}` }],
            isError: true
          };
        }
      }
    );
    
    // Tool 2: Describe Table
    server.tool(
      "describe_table",
      "Get column details for a specific table (e.g. 'rooms', 'devices', 'commissioning_log').",
      {
        tableName: z.string().describe("Table name"),
        schema: z.string().optional().describe("Schema name (default: public)")
      },
      async ({ tableName, schema = "public" }) => {
        // Validate table name to prevent SQL injection
        if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
          return {
            content: [{ type: "text", text: "Error: Invalid table name format" }],
            isError: true
          };
        }
    
        try {
          const result = await pool.query(`
            SELECT
              column_name,
              data_type,
              character_maximum_length,
              is_nullable,
              column_default,
              (SELECT COUNT(*) FROM information_schema.key_column_usage k
               WHERE k.column_name = c.column_name
               AND k.table_name = c.table_name) > 0 as is_key
            FROM information_schema.columns c
            WHERE table_schema = $1 AND table_name = $2
            ORDER BY ordinal_position
          `, [schema, tableName]);
    
          if (result.rows.length === 0) {
            return {
              content: [{ type: "text", text: `Table '${tableName}' not found in schema '${schema}'` }],
              isError: true
            };
          }
    
          return {
            content: [{
              type: "text",
              text: JSON.stringify({
                table: tableName,
                schema,
                columns: result.rows
              }, null, 2)
            }]
          };
        } catch (err: any) {
          return {
            content: [{ type: "text", text: `Database error: ${err.message}` }],
            isError: true
          };
        }
      }
    );
    
    // Tool 3: Read-Only Query
    server.tool(
      "query_data",
      "Run a read-only SQL SELECT against the AV database. SELECT only.",
      {
        query: z.string().describe("SQL SELECT query"),
        limit: z.number().optional().describe("Max rows (default: 100, max: 1000)")
      },
      async ({ query, limit = 100 }) => {
        const safeLimit = Math.min(limit, 1000);
    
        // Normalize and validate query
        const normalized = query.trim().toLowerCase();
    
        // Only allow SELECT queries
        if (!normalized.startsWith("select")) {
          return {
            content: [{ type: "text", text: "Error: Only SELECT queries are allowed" }],
            isError: true
          };
        }
    
        // Block dangerous patterns
        const forbidden = [
          "insert", "update", "delete", "drop", "alter", "truncate",
          "create", "grant", "revoke", "execute", "exec"
        ];
    
        for (const word of forbidden) {
          if (normalized.includes(word)) {
            return {
              content: [{ type: "text", text: `Error: Query contains forbidden keyword: ${word}` }],
              isError: true
            };
          }
        }
    
        try {
          // Force read-only transaction and add limit
          const client = await pool.connect();
          try {
            await client.query("SET TRANSACTION READ ONLY");
    
            // Remove any existing LIMIT and add our safe limit
            const limitedQuery = query.replace(/\s+limit\s+\d+/gi, "") + ` LIMIT ${safeLimit}`;
    
            const result = await client.query(limitedQuery);
    
            return {
              content: [{
                type: "text",
                text: JSON.stringify({
                  rowCount: result.rowCount,
                  rows: result.rows,
                  fields: result.fields.map(f => ({ name: f.name, dataType: f.dataTypeID }))
                }, null, 2)
              }]
            };
          } finally {
            client.release();
          }
        } catch (err: any) {
          return {
            content: [{ type: "text", text: `Query error: ${err.message}` }],
            isError: true
          };
        }
      }
    );
    
    // Tool 4: Sample a Table
    server.tool(
      "sample_table",
      "Pull a small random sample of rows so the agent can understand a table's shape.",
      {
        tableName: z.string().describe("Table name"),
        sampleSize: z.number().optional().describe("Rows to sample (default: 5, max: 20)")
      },
      async ({ tableName, sampleSize = 5 }) => {
        // Validate table name
        if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
          return {
            content: [{ type: "text", text: "Error: Invalid table name format" }],
            isError: true
          };
        }
    
        const safeSampleSize = Math.min(sampleSize, 20);
    
        try {
          const result = await pool.query(`
            SELECT * FROM "${tableName}"
            ORDER BY RANDOM()
            LIMIT $1
          `, [safeSampleSize]);
    
          return {
            content: [{
              type: "text",
              text: JSON.stringify({
                table: tableName,
                sampleSize: result.rows.length,
                rows: result.rows
              }, null, 2)
            }]
          };
        } catch (err: any) {
          return {
            content: [{ type: "text", text: `Sample error: ${err.message}` }],
            isError: true
          };
        }
      }
    );
    
    // Tool 5: Table Statistics
    server.tool(
      "table_stats",
      "Row count, on-disk size, and indexes for a table.",
      {
        tableName: z.string().describe("Table name")
      },
      async ({ tableName }) => {
        // Validate table name
        if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
          return {
            content: [{ type: "text", text: "Error: Invalid table name format" }],
            isError: true
          };
        }
    
        try {
          const [countResult, sizeResult, indexResult] = await Promise.all([
            pool.query(`SELECT COUNT(*) as row_count FROM "${tableName}"`),
            pool.query(`
              SELECT pg_size_pretty(pg_total_relation_size($1)) as total_size,
                     pg_size_pretty(pg_table_size($1)) as table_size,
                     pg_size_pretty(pg_indexes_size($1)) as index_size
            `, [tableName]),
            pool.query(`
              SELECT indexname, indexdef
              FROM pg_indexes
              WHERE tablename = $1
            `, [tableName])
          ]);
    
          return {
            content: [{
              type: "text",
              text: JSON.stringify({
                table: tableName,
                rowCount: parseInt(countResult.rows[0].row_count),
                size: sizeResult.rows[0],
                indexes: indexResult.rows
              }, null, 2)
            }]
          };
        } catch (err: any) {
          return {
            content: [{ type: "text", text: `Stats error: ${err.message}` }],
            isError: true
          };
        }
      }
    );
    
    // HTTP transport setup
    const transports: Record<string, StreamableHTTPServerTransport> = {};
    
    app.post("/mcp", async (req, res) => {
      const sessionId = req.headers["mcp-session-id"] as string;
      let transport = transports[sessionId];
    
      if (!transport) {
        transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: () => randomUUID()
        });
        await server.connect(transport);
      }
    
      const newSessionId = (transport as any)._sessionId;
      if (newSessionId) transports[newSessionId] = transport;
    
      await transport.handleRequest(req, res, req.body);
    });
    
    app.get("/mcp", async (req, res) => {
      const sessionId = req.headers["mcp-session-id"] as string;
      const transport = transports[sessionId];
      if (transport) await transport.handleRequest(req, res);
      else res.status(404).json({ error: "Session not found" });
    });
    
    // Graceful shutdown
    process.on("SIGTERM", async () => {
      console.log("Shutting down...");
      await pool.end();
      process.exit(0);
    });
    
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
      console.log(`AV database MCP server running at http://localhost:${PORT}/mcp`);
    });

    How you shape your tools determines whether the agent makes good decisions on a real service call.

    Tool Count Guidelines#

    Tool Count Assessment Recommendation
    3-10 Ideal Easy for the agent to navigate
    10-20 Good Group related tools
    20-50 Complex Agent may struggle to choose correctly
    50+ Too many Consolidate into workflow tools

    A field-tech assistant with 6 well-named tools (look up room, fetch device status, open ticket, attach photo, mark resolved, escalate) outperforms one with 40 finely sliced ones.


    Your MCP server needs to verify requests come from authorized callers (your AVCodex agent, your dispatch tooling, you).

    Note: Need per-user authentication so each tech connects with their own ServiceNow, Crestron XiO, or Microsoft account? See Consumer OAuth for Custom MCP Servers for the full setup.

    Bearer Token Authentication#

    typescript
    // Middleware to verify Bearer token
    function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
      const authHeader = req.headers.authorization;
    
      if (!authHeader || !authHeader.startsWith("Bearer ")) {
        return res.status(401).json({ error: "Missing or invalid Authorization header" });
      }
    
      const token = authHeader.substring(7);
    
      // Verify token (implement your own logic)
      if (!isValidToken(token)) {
        return res.status(401).json({ error: "Invalid token" });
      }
    
      next();
    }
    
    // Apply to MCP endpoints
    app.post("/mcp", authenticate, async (req, res) => {
      // ... handler
    });
    
    app.get("/mcp", authenticate, async (req, res) => {
      // ... handler
    });

    API Key Authentication#

    typescript
    function authenticateApiKey(req: express.Request, res: express.Response, next: express.NextFunction) {
      const apiKey = req.headers["x-api-key"] as string;
    
      if (!apiKey) {
        return res.status(401).json({ error: "Missing X-API-Key header" });
      }
    
      // Verify API key
      if (!isValidApiKey(apiKey)) {
        return res.status(401).json({ error: "Invalid API key" });
      }
    
      next();
    }

    Pick a deployment that matches your ops posture: managed cloud for simplicity, on-prem for sensitive control-system data.

    Cloud Run Deployment#

    dockerfile
    # Dockerfile
    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY . .
    RUN npm run build
    EXPOSE 8080
    CMD ["node", "dist/server.js"]
    bash
    # Deploy to Cloud Run
    gcloud run deploy avcodex-mcp-server \
      --source . \
      --region us-central1 \
      --no-allow-unauthenticated \
      --set-env-vars="DATABASE_URL=..."

    Development with ngrok#

    bash
    # Start your local server
    npm run dev
    
    # In another terminal, expose it
    ngrok http 3000
    
    # Use the ngrok URL in AVCodex
    # https://abc123.ngrok-free.app/mcp

    Before connecting your server to a production AVCodex agent, verify:

    • HTTPS only. No plaintext on any path that touches client data.
    • Bearer token or API key required on every MCP endpoint.
    • Read-only DB connections for data tools. No write paths the agent can call by accident.
    • Input validation on every tool argument (table names, room IDs, device serials).
    • Rate limits in place so a runaway agent loop cannot hammer your back-ends.
    • Logs that capture tool name, caller, arguments, and outcome (see the audit logs guide).
    • Secrets in a real secret store, not env files checked into Git.

    Once your MCP server is deployed:

    1. Open your AVCodex agent's builder.
    2. Add a new Custom MCP Server.
    3. Paste the URL, transport, and credential.
    4. Save. AVCodex calls tools/list and shows the tools your server exposed.
    5. Enable the tools you want the agent to use, write a short description for each, and test.

    Configuration Fields#

    Field Description Example
    Server URL Full URL to your MCP endpoint https://avcodex-mcp.run.app/mcp
    Transport Protocol type HTTP (recommended)
    Auth Type How to authenticate Bearer or API Key
    Token/Key Your authentication credential sk-abc123...

    After saving, AVCodex calls your server's tools/list method and displays the available tools. Enable the ones you want the agent to use.


    Common Issues#

    "Connection refused"

    • Check that your server is running and reachable from the internet.
    • Verify HTTPS is correctly configured.
    • Check firewall rules for inbound traffic.

    "Authentication failed"

    • Verify your token or API key is correct.
    • Confirm the Authorization header format matches what your server expects.
    • Ensure tokens have not expired.

    "Tool not found"

    • Run tools/list directly to confirm the tool is registered.
    • Check that tool names contain only safe characters.
    • Verify the tool is enabled in AVCodex.

    Slow response times

    • Add connection pooling on database tools.
    • Cache frequently requested data (room records, device profiles).
    • Move your server to a region close to the AVCodex agent runtime.

    Testing Your Server#

    Use curl to hit your MCP server directly:

    bash
    # Test tools/list
    curl -X POST https://your-server.com/mcp \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer your-token" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
    
    # Test tools/call
    curl -X POST https://your-server.com/mcp \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer your-token" \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"your_tool","arguments":{}}}'

    AVCodex Documentation:

    • Consumer OAuth for Custom MCP Servers. Per-user OAuth for third-party services.
    • AVCodex MCP Server. Connect Claude Code to build and manage your AVCodex agents.
    • Pro Actions Getting Started. Pre-built MCP integrations.
    • Pro Actions Examples. Real AV-ops workflows.
    • Custom Actions Guide. Simpler HTTP-based integrations.
    • API Reference. Full API documentation.

    Official MCP Documentation:

    Example Repositories:

    Deployment Guides:

    *AVCodex · Your AV expertise. Amplified by AI.*

    Was this helpful?
    Edit this page →