14 minutes Read

Published On

A Development Guide to Build Custom Tools for NetSuite AI Connector

In our previous blog about the complete setup guide for NetSuite AI Connector, we covered how to set up the NetSuite AI Connector from scratch. Before we move forward and explore how you can build your first custom tool, it’s critical to be aware of two official NetSuite resources that outline the foundations you’ll need:

Now, let’s take things a step further and learn to build custom tools that extend the connector’s functionality.

Custom tools let you expose business-specific operations to the AI Connector. For example, retrieving detailed sales orders, emailing PDFs of transactions, or sending tailored emails.

Here’s the project structure we’ll use throughout this guide:

mcp-custom-tools/
├── src/
│   ├── FileCabinet/
│   │   └── SuiteApps/com.foliothree.f3mcptools/
│   │       ├── f3_salesorders_ct_schema.json
│   │       ├── f3_salesorders_ct.js
│   ├── Objects/CustomTools/
│   │   └── customtool_f3_salesorders_ct.xml
│   ├── deploy.xml
│   ├── manifest.xml
├── package.json
├── project.json
├── suitecloud.config.js

This folder structure keeps schemas, scripts, and XML configurations organized and ready for SuiteCloud deployment.

Step 1: Define Custom Tool Schema

Every tool begins with a schema definition (.json) that tells the AI Connector what the tool does, what input it expects, and how it behaves.

Here’s a snippet from our f3_salesorders_ct_schema.json:

{
    "tools": [
        {
            "name": "getSalesOrderInfo",
            "description": "Retrieve comprehensive information about a specific sales order, including header fields, line items with expected ship dates, tax details, and additional metadata like department and location. Each line item includes expected ship date from the line or falls back to header ship date",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "orderId": {
                        "type": "string",
                        "description": "The internal ID of the sales order to retrieve detailed information for"
                    }
                },
                "required": ["orderId"],
                "nullable": []
            },
            "outputSchema": {
                "type": "object",
                "properties": {
                    "result": {
                        "type": "string",
                        "description": "JSON stringified sales order data containing comprehensive details, or empty string if error occurred"
                    },
                    "error": {
                        "type": "string",
                        "description": "Error message if execution fails, or empty string if successful"
                    }
                },
                "required": ["result"],
                "nullable": ["error"]
            },
            "annotations": {
                "title": "Get Sales Order Information",
                "readOnlyHint": true,
                "idempotentHint": true,
                "openWorldHint": false
            }
        }
    ]
}

Key points in the above schema are:

  • name → Function name to be exposed.
  • description → Human-readable explanation of what the tool does.
  • inputSchema → Input fields required to run the tool, along with nullable to explicitly define which inputs (if any) can be empty.
  • outputSchema → Expected response structure (result, error, etc.) so that ChatGPT or any LLM can reliably parse the tool’s output.
  • nullable → Ensures compatibility with ChatGPT by specifying which fields may return empty values (avoids schema mismatch errors).
  • annotations → Hints for connector behavior (e.g., read-only, idempotent, open-world).

Schemas act as a contract between the AI Connector and your script. Without this, the automation connector won’t know how to interpret or execute the function.

Step 2: Implement Business Logic in JavaScript

With the schema in place, let’s implement the actual functionality in a SuiteScript 2.1 file (f3_salesorders_ct.js).

Here’s an example of getSalesOrderInfo:

/**
* @NApiVersion 2.1
* @NModuleScope Public
*/
define(['N/record'], function (record) {
    return {
        getSalesOrderInfo: function (params) {
            const orderId = params.orderId;
            if (!orderId) {
                return {error: 'Missing order ID'};
            }
            try {
                const salesOrder = record.load({
                    type: record.Type.SALES_ORDER,
                    id: orderId,
                });

                // Get header fields with additional information
                let salesOrderData = {
                    id: orderId,
                    tranId: salesOrder.getValue('tranid'),
                    customerId: salesOrder.getValue('entity'),
                    customerName: salesOrder.getText('entity'),
                    date: salesOrder.getValue('trandate'),
                    dueDate: salesOrder.getValue('duedate'),
                    shipDate: salesOrder.getValue('shipdate'), // Adding ship date from header
                    status: salesOrder.getValue('status'),
                    statusText: salesOrder.getText('status'),
                    poNumber: salesOrder.getValue('otherrefnum'),
                    memo: salesOrder.getValue('memo'),
                    salesRep: salesOrder.getValue('salesrep'),
                    salesRepName: salesOrder.getText('salesrep'),
                    department: salesOrder.getValue('department'),
                    departmentName: salesOrder.getText('department'),
                    location: salesOrder.getValue('location'),
                    locationName: salesOrder.getText('location'),
                    subtotal: salesOrder.getValue('subtotal'),
                    taxtotal: salesOrder.getValue('taxtotal'),
                    total: salesOrder.getValue('total'),
                    currency: salesOrder.getValue('currency'),
                    currencyName: salesOrder.getText('currency'),
                    items: [],
                };

                // Get the header ship date as fallback for line items
                const headerShipDate = salesOrder.getValue('shipdate');

                // Get line items with additional details
                const lineCount = salesOrder.getLineCount({
                    sublistId: 'item',
                });

                for (let i = 0; i < lineCount; i++) {
                    // Get expected ship date from line item, fallback to header ship date
                    const lineExpectedShipDate = salesOrder.getSublistValue({
                        sublistId: 'item',
                        fieldId: 'expectedshipdate',
                        line: i,
                    });
                   
                    // Use line expected ship date if available, otherwise use header ship date
                    const effectiveShipDate = lineExpectedShipDate || headerShipDate;
                   
                    salesOrderData.items.push({
                        line: i + 1,
                        itemId: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'item',
                            line: i,
                        }),
                        itemName: salesOrder.getSublistText({
                            sublistId: 'item',
                            fieldId: 'item',
                            line: i,
                        }),
                        itemType: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'itemtype',
                            line: i,
                        }),
                        quantity: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'quantity',
                            line: i,
                        }),
                        rate: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'rate',
                            line: i,
                        }),
                        amount: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'amount',
                            line: i,
                        }),
                        description: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'description',
                            line: i,
                        }),
                        expectedShipDate: lineExpectedShipDate, // Line-specific expected ship date
                        effectiveShipDate: effectiveShipDate, // Either line expected ship date or header ship date
                        shipDateSource: lineExpectedShipDate ? 'line' : 'header', // Indicator of where the date came from
                        taxCode: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'taxcode',
                            line: i,
                        }),
                        taxAmount: salesOrder.getSublistValue({
                            sublistId: 'item',
                            fieldId: 'tax1amt',
                            line: i,
                        }),
                    });
                }

                return {
                    result: JSON.stringify(salesOrderData),
                    error: ''
                }
            } catch (error) {
                return {
                    result: '',
                    error: `Error getting sales order info ${orderId}: ${error.toString()}`,
                };
            }
        }
    };
});

This function:

  • Loads a sales order record by ID.
  • Collects header details (transaction ID, customer name, total, etc.).
  • Loops through line items and captures quantity, rate, name, etc.
  • Returns a JSON payload that the AI Connector can consume.

You can extend this further (as in your full implementation) to include ship dates, tax codes, departments, etc.

Step 3: XML Configuration

NetSuite requires you to register your tool with a .xml file under Objects/CustomTools/.

Here’s the customtool_f3_salesorders_ct.xml:

<tool scriptid="customtool_f3_salesorders_ct"> 
  <name>Sales Orders Tools Custom Implementation</name> 
  <scriptfile>[/SuiteApps/com.foliothree.f3mcptools/f3_salesorders_ct.js]</scriptfile> 
  <rpcschema>[/SuiteApps/com.foliothree.f3mcptools/f3_salesorders_ct_schema.json]</rpcschema> 
  <exposeto3rdpartyagents>T</exposeto3rdpartyagents>
</tool>

This XML ties together:
Script file (.js)
Schema file (.json)
Exposure flag (T to make available for external AI agents)
Without this step, NetSuite won’t recognize your new tool as part of the SuiteApp.

Step 4: Deployment Setup

To push everything to NetSuite, we rely on two key XML files:

deploy.xml – defines what gets deployed:

<deploy>
  <files>
    <path>~/FileCabinet/SuiteApps/com.foliothree.f3mcptools/*</path>
  </files>
  <objects>
    <path>~/Objects/CustomTools/*</path>
  </objects>
</deploy>

manifest.xml – describes the SuiteApp project:
<manifest projecttype="SUITEAPP">
  <publisherid>com.foliothree</publisherid>
  <projectid>f3mcptools</projectid>
  <projectname>Folio3 Custom Tools</projectname>
  <projectversion>1.0.0</projectversion>
  <dependencies>
    <features>
      <feature required="true">SERVERSIDESCRIPTING</feature>
    </features>
  </dependencies>
</manifest>

Together, these files tell NetSuite:

  • Which files to install.
  • Which objects to register.
  • What features are required (like server-side scripting).

Step 5: Deploy & Test

Once your files are ready:

  1. Run deployment using SuiteCloud CLI:
    suitecloud project:deploy
  2. Verify the SuiteApp deployment inside NetSuite’s File Cabinet under SuiteApps -> com.foliothree.f3mcptools. (or if its ACP) SuiteScripts -> Customizations -> com.foliothree.f3mcptools
  3. Test your tool by invoking it through the AI Connector with sample inputs.

Example Test Input for getSalesOrderInfo:

{
  "orderId": "12345"
}

Expected Output:

{
  "id": "12345",
  "tranId": "SO1001",
  "customerName": "ABC Corp",
  "total": "1500.00",
  ...
  "items": [
    {"itemName": "Laptop", "quantity": 2, "rate": 500},
    {"itemName": "Mouse", "quantity": 5, "rate": 50},
    ...
  ]
}

Custom Tool Deployment on Claude.ai:

Example Prompt via Claude.ai:

Custom Tool Deployment on ChatGPT:

Example Prompt via ChatGPT:

Wrapping It Up

By now, you’ve seen how to:

  • Structure your project for custom NetSuite AI Connector tools.
  • Write JSON schemas to define inputs and outputs.
  • Implement robust SuiteScript logic for business operations.
  • Configure XML to register tools with NetSuite.
  • Deploy and test your tools in the SuiteCloud environment.

With this foundation, you can start building a whole ecosystem of AI-driven NetSuite integrations to automate sales order insights and manage vendor communications.

If you need further assistance, you can schedule a call with us and we will show you a practical demo.

FAQs

1. Can I build multiple tools in a single SuiteApp project?
Yes! You can define multiple schemas and JS implementations, then register them in separate XML files under Objects/CustomTools/.

2. Can I connect these tools to external AI systems?
Absolutely. The main purpose of the AI Connector is to let AI agents consume NetSuite data and actions securely.

3. How to un-deploy a custom tool?

  • The challenge: Simply setting <exposeto3rdpartyagents>F</exposeto3rdpartyagents> makes the tool invisible to AI clients, but the underlying script, schema, and XML files remain in NetSuite.
  • Resolution: If you deploy a SuiteApp and remove objects from the deploy.xml, redeploying can delete those omitted objects from the account (keeping only what remains in deploy.xml).

However, with an Account Customization Project (ACP), simply removing objects from deploy.xml will not delete them in the target account. Unwanted components must be manually removed – the deployment is non-destructive.

4. Can we define outputSchema just like inputSchema?

Yes, outputSchema is supported. The official NetSuite MCP docs confirm that you can (and in some cases should) define an outputSchema for your custom tool, especially if you want better compatibility with AI clients like ChatGPT MCP.

Here’s what to know:

  • Without outputSchema – Your tool still works, but the LLM has to “guess” how to interpret the JSON you return. That’s when warnings or misinterpretations happen.
  • With outputSchema – You explicitly define the format and types of the response, making it easier for the LLM to consume and reducing hallucinations.

5. What’s supported in inputSchema?

  • inputSchema.properties support key-value pairs of type and description for each property. The types supported are: string, number, and boolean.
  • Arrays and nested objects may break depending on how the LLM interprets them. Many developers recommend using strings to encode complex structures (e.g., pass a JSON string instead of a raw object).

6. What are the restrictions and limitations of the Netsuite AI Connector ?

The N/http and N/https modules are not supported in custom tool scripts.

7. Can I set up a Netsuite AI Connector via SuiteApp ?

Yes, if you have a SuiteApp (as set up earlier in this blog) with Custom Tools then you can generate it’s MCP server URL as following:

https://<accountid>.suitetalk.api.netsuite.com/services/mcp/v1/suiteapp/<applicationid> – Returns only the custom tools exposed by the specified SuiteApp. The structure of <applicationid> is: <publisherid>.<projectid> where <publisherid> could be, for example, “com.foliothree”, and <projectid>, for instance, “f3mcptools”.

8. How should I manage the access management of the MCP Tools ?

  • Grant MCP permission only to users who require it.
  • Do not assign MCP permission to users with high privileges. NetSuite does not allow Administrator roles or roles that have full permissions to access NetSuite features to use MCP.
  • Using the SuiteApp specific URL to expose only specific set of tools
  • Developer Mode (available on ChatGPT Pro/Plus) gives full MCP client support, i.e. both read and write access to connected tools.

In contrast, in Enterprise plans connectors are disabled by default and admins control which connectors (and what operations) users can use.

9. Can I use the Netsuite AI Connector Service in the healthcare Industry ?

The NetSuite AI Connector Service can’t be activated for healthcare customers with a signed Business Associate Agreement (BAA), as it has not yet been assessed for its compliance with the US Health Insurance Portability and Accountability Act (HIPAA).

Meet the Author

Muhammad Nasir Uddin Mahmood

Senior Software Engineer

I’m Nasir Mahmood - passionate about innovation, continuous learning, and building solutions that create real impact. I thrive on staying ahead with the latest in technology and science, always curious about what’s next and how it can be applied in practical, meaningful ways. Outside of work, I keep a balance between mind and body. You’ll often find me in the gym working out to stay sharp, or immersed in competitive video games, where strategy and focus push me to bring my A-game.

Table of Contents

Contact Us

By submitting this form, you agree to our privacy policy and terms of service.

Related resources you might be interested in

Deeper in to NetSuite, Development & Customization
Deeper in to NetSuite, Operations & Supply Chain

We'd love to help you with all your NetSuite needs

Folio3 Your Top Choice:

Middle East Partner 2025
education award 2025
Winner Award
Software and IT Services 2024
Financial-Services-2023
SuiteCommerce 2023

Let's discuss your NetSuite needs

Hello, How can we help you?