Skip to content
SeamSetTalk to us

All guides

NetSuite Custom Tool script type: build and migrate an MCP tool (toolset format, as of 2026.2)

By Arnaud de Turckheim

Last verified against NetSuite 2026.2

In short

  • A NetSuite custom tool is a SuiteScript 2.1 script of the Custom Tool script type (@NScriptType CustomTool) that the NetSuite AI Connector Service, Oracle’s MCP (Model Context Protocol) server, offers to AI clients such as Claude and ChatGPT.
  • A toolset is three files: the script, a JSON-RPC schema describing each tool, and a toolset SDF (SuiteCloud Development Framework) object with <exposetoaiconnector>T</exposetoaiconnector>. You deploy it with the SuiteCloud CLI and connect through the /services/mcp/v1/all URL.
  • 2025 tools no longer deploy (September 2026 minor release) and stop executing in 2027.1. The fix: tool becomes toolset, customtool_ becomes custtoolset_, exposeto3rdpartyagents becomes exposetoaiconnector, plus @NScriptType CustomTool and async methods. See the migration checklist.
  • A tool runs with the connected user’s role, never Administrator. It cannot use N/http, N/https, N/llm or N/sftp, and gets 1,000 usage units and 300 seconds per execution.
  • The toolset’s <permissions> only decide who sees the tool. The user’s role decides what it can read or change.

As of NetSuite 2026.2 (September 2026), with Oracle’s Help Center read on September 26, 2026.

What you need

For NetSuite developers who know SuiteScript and SDF projects.

  • Features on in Setup > Company > Enable Features, SuiteCloud subtab: Server SuiteScript, OAuth 2.0 and SuiteCloud Development Framework (Oracle’s prerequisites for custom tools), and Custom Records, which Oracle lists for SDF.
  • A role to deploy with: Oracle’s standard Developer role, assigned by an administrator. Oracle advises against Administrator or full-privilege roles for routine deployments.
  • The SuiteCloud CLI for Node.js (@oracle/suitecloud-cli), which requires Node.js 24 LTS and Oracle JDK 17 or 21.
  • A sandbox to deploy to first.
  • A non-Administrator AI role with MCP Server Connection and Log in using OAuth 2.0 Access Tokens: see A least-privilege NetSuite role for AI.
  • An Administrator, the only role that can activate or deactivate toolsets.
  • Claude Pro or higher (Oracle’s requirement), or ChatGPT with developer mode. OpenAI’s developer guide lists Plus, Pro, Business, Enterprise and Education, on the web; its help center says write actions are rolling out in beta to Business, Enterprise and Edu (plan details). The AI Connector itself is free and uses no AI Units.

When a custom tool beats the standard tools

Oracle suggests custom tools for “Specialized workflows” and “Custom automation”. Three cases justify the work:

Case Example Why the standard tools fall short
Business logic Credit check: limit, open balance, unbilled orders, overdue amount, one decision With ns_runCustomSuiteQL, the model writes its own SuiteQL (NetSuite’s SQL dialect) each time and may pick other fields or another rule
Schema context Your custom records, fields and internal terms, with plain descriptions ns_getSuiteQLMetadata returns field metadata (types, descriptions, joins), not your rules or vocabulary
Guarded writes Change one field on one record type, only after your checks pass ns_updateRecord can update any REST-available record the role may edit

Keep the standard tools for open questions, reports and saved searches. Plative adds a test: on a heavily customized data model, build a simple custom tool for the questions that come up often (Plative, NetSuite MCP deep dive).

Seen in practice. Prolecto ran one client accomplishment prompt in Claude twice: with the standard tools only, then with all tools, its custom tools included (Prolecto, standard vs custom tools). In the second run, Claude used the custom tools and ran no ad hoc SuiteQL, though it could.

By Prolecto’s count, the custom run found more (46 accomplishments against 33), left out support requests the standard run had counted, made fewer calls, and took about 6 minutes 20 seconds instead of 12.5 (Prolecto, results).

To compare runs yourself, count the tool calls Claude shows in each chat, or check the integration record’s execution log (step 7 below). Prolecto also added a diagnostics section to each report (Prolecto, diagnostics).

Anatomy of a custom tool

  1. Script (.js, SuiteScript 2.1). The header must include @NScriptType CustomTool and every tool method must be async. There are no predefined entry points: the script returns one method per tool.
  2. JSON-RPC schema (.json). A root object { "tools": [ ... ] }, one definition per tool. The client reads each name and description to decide when to call it.
  3. toolset SDF object (.xml in Objects/). It links script and schema, exposes them to the AI Connector and lists the permissions needed to see the tools.
  4. Optional: a self-contained .html file for an MCP App (a tool with its own interface, declared with _meta.ui.resourceUri).

Custom tool scripts have no Script or Script Deployment pages. Once deployed, these files can be edited only through SDF, not in the File Cabinet.

Method contract. Each tool method receives one object keyed by the inputSchema property names, which Oracle’s SDF agent skill calls “Flat, untyped”: convert and validate every value. Return a JSON string, as Oracle’s best practices ask, and return errors as data, not exceptions (the skill’s rule).

Oracle’s samples mostly call .promise APIs such as query.runSuiteQL.promise, but also call save() synchronously inside async methods. The Help Center does not say which is required.

Schema properties NetSuite accepts

The file must be valid JSON and use “only the properties documented”, or “deployment fails validation”.

Property Required Notes
name Yes Method name exactly as in the script, unique in the file
description Yes How the client decides when to use the tool
inputSchema: type, properties, required Yes type is object; property types string, number, boolean
inputSchema.nullable For ChatGPT An array of property names, not a boolean. Oracle’s sample: "nullable": []
outputSchema: type, properties, required, nullable For ChatGPT Adds array (with items). Oracle’s sample: "nullable": ["error"]
annotations: title, readOnlyHint, idempotentHint, openWorldHint, destructiveHint No destructiveHint defaults to true, the others to false. idempotentHint and destructiveHint apply only when readOnlyHint is false
_meta.ui.resourceUri No MCP Apps only

Oracle does not document JSON Schema keywords such as enum, default or minimum. Treat them as unsupported until tested.

The toolset SDF object (XML)

Element Required Rule (Oracle’s XML reference)
scriptid attribute Yes Starts with custtoolset; lowercase, alphanumeric and underscores; up to 27 characters; matches the XML file name
<name> Yes Toolset name
<scriptfile> Yes A .js file
<rpcschema> Yes A .json file
<exposetoaiconnector> No T makes the toolset available through the AI Connector. Default F
<permissions> > <permission> > <permkey>, <permlevel> No Permissions a role needs to see the tools

The feature dependency is SERVERSIDESCRIPTING. There is no run-as-role element.

Restrictions to design for

Restriction Detail
Modules N/http, N/https, N/llm and N/sftp are not supported. Oracle lists no allowed set; its samples use N/query, N/record and N/log
Usage units 1,000 per execution of the tool. One query.runSuiteQL call costs 10
Time 300 seconds, then SSS_TIME_LIMIT_EXCEEDED
Role The connected user’s role; never Administrator or “roles that have full permissions”; “‘Run as role’ is disabled for all tools”
Other code No Suitelets, no scripts with elevated privileges, no HTTP requests to external destinations
Triggered customizations User event and workflow action scripts run as the AI Connector role; workflows too, “If not set to run as administrator”

Oracle also notes that AI clients “usually support up to ~5,000 rows per call”: page and bound your results.

Read literally, a workflow set to run as administrator still does so when a tool triggers it: review workflows on records your tools write.

Build a credit check tool

This illustrative, read-only tool checks one customer against a simple credit rule. It follows Oracle’s Help Center sample and GitHub Sample-Toolsets project, including its SuiteQL fields. It was run against a mocked N/query, not in a NetSuite account: deploy it to a sandbox first.

NetSuite’s Customer Credit Limit Handling preference (Setup > Accounting > Preferences > Accounting Preferences: Ignore, Warn Only or Enforce Holds) acts when a sales order or invoice is entered. A tool lets Claude answer before any order exists, with your own rules such as overdue balance.

1. Create the project

npm install -g @oracle/suitecloud-cli
suitecloud project:create -i

Choose an account customization project (ACP) for a rule that lives in one account; a SuiteApp project suits tools shipped to several accounts. Accept the Jest unit testing option (see the FAQ). Target layout:

src/
  manifest.xml      (generated; add the feature dependency in step 5)
  deploy.xml        (generated; includes ~/FileCabinet/* and ~/Objects/*)
  FileCabinet/SuiteScripts/toolset/acme_credit_tools.js
  FileCabinet/SuiteScripts/toolset/acme_credit_tools_schema.json
  Objects/custtoolset_acme_credit.xml

2. The script

/**
 * acme_credit_tools.js
 * @NApiVersion 2.1
 * @NScriptType CustomTool
 */
define(['N/query', 'N/log'], function (query, log) {
    return {
        acme_checkCustomerCredit: async function (args) {
            try {
                const customerId = String(args.customerId || '');
                const orderAmount = Number(args.orderAmount);
                if (!/^\d+$/.test(customerId) || !Number.isFinite(orderAmount) || orderAmount < 0) {
                    return JSON.stringify({ error: 'customerId must be a numeric internal ID, orderAmount a number of 0 or more' });
                }
                const result = await query.runSuiteQL.promise({
                    query: 'SELECT creditlimit, balancesearch, overduebalancesearch, unbilledorderssearch FROM customer WHERE id = ?',
                    params: [customerId]
                });
                const rows = result.asMappedResults();
                if (rows.length === 0) {
                    return JSON.stringify({ error: 'Customer not found or not visible to your role' });
                }
                const c = rows[0];
                const creditLimit = (c.creditlimit == null || c.creditlimit === '') ? null : Number(c.creditlimit);
                const overdue = Number(c.overduebalancesearch || 0);
                const exposure = Number(c.balancesearch || 0) + Number(c.unbilledorderssearch || 0) + orderAmount;

                // Illustrative rule: replace with your credit policy,
                // including what it does when no credit limit is set.
                const reasons = [];
                if (creditLimit === null) reasons.push('No credit limit set');
                else if (exposure > creditLimit) reasons.push('Exposure after this order exceeds the credit limit');
                if (overdue > 0) reasons.push('Overdue balance');

                const withinPolicy = reasons.length === 0;
                log.audit('acme_checkCustomerCredit', JSON.stringify({ customerId: customerId, orderAmount: orderAmount, withinPolicy: withinPolicy }));
                return JSON.stringify({
                    customerId: customerId,
                    creditLimit: creditLimit,
                    overdueBalance: overdue,
                    exposureAfterOrder: exposure,
                    withinPolicy: withinPolicy,
                    reasons: reasons
                });
            } catch (e) {
                log.error('acme_checkCustomerCredit', e.toString());
                return JSON.stringify({ error: 'Credit check failed. Details are in the Script Execution Logs.' });
            }
        }
    };
});

The customer ID is a bound parameter (?), never SuiteQL text from the model, and the output carries numbers and IDs, not free text. The client gets a generic error while the details go to the logs, as Oracle’s SDF agent skill recommends. The log.audit line records each successful call.

In a multi-currency account, check which currency the balance fields use.

3. The schema

{
    "tools": [
        {
            "name": "acme_checkCustomerCredit",
            "description": "Checks one customer against Acme's credit policy before a new order. Returns the credit limit, overdue balance, exposure after the order, and whether the order is within policy. Read-only: never changes records.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "customerId": { "type": "string", "description": "Internal ID of the customer record (digits only, for example 1234), not the customer name" },
                    "orderAmount": { "type": "number", "description": "Total amount of the new order, 0 or more" }
                },
                "required": ["customerId", "orderAmount"]
            },
            "annotations": {
                "title": "Check customer credit",
                "readOnlyHint": true,
                "openWorldHint": false
            }
        }
    ]
}

The acme_ prefix follows Oracle’s advice to “Use a common prefix” for tools from one publisher.

4. The toolset object

Objects/custtoolset_acme_credit.xml:

<toolset scriptid="custtoolset_acme_credit">
  <name>Acme Credit Tools</name>
  <scriptfile>[/SuiteScripts/toolset/acme_credit_tools.js]</scriptfile>
  <rpcschema>[/SuiteScripts/toolset/acme_credit_tools_schema.json]</rpcschema>
  <exposetoaiconnector>T</exposetoaiconnector>
  <permissions>
    <permission>
      <permkey>LIST_CUSTJOB</permkey>
      <permlevel>VIEW</permlevel>
    </permission>
  </permissions>
</toolset>

LIST_CUSTJOB is the Customers permission in Oracle’s permission index. Oracle’s customer sample asks for it at EDIT because some of its tools write.

5. Declare the feature dependency

The generated manifest.xml does not declare SERVERSIDESCRIPTING, the feature toolset depends on. From the project folder, run suitecloud project:adddependencies, or edit src/manifest.xml like Oracle’s sample (keep your project name):

<manifest projecttype="ACCOUNTCUSTOMIZATION">
  <projectname>Acme Credit Tools</projectname>
  <frameworkversion>1.0</frameworkversion>
  <dependencies>
    <features>
      <feature required="true">SERVERSIDESCRIPTING</feature>
    </features>
  </dependencies>
</manifest>

Deploy with the SuiteCloud CLI

From the project folder:

suitecloud account:setup
suitecloud project:validate
suitecloud project:deploy --dryrun
suitecloud project:deploy

account:setup opens the browser and saves an auth ID for one account and role: log in to the sandbox with the Developer role, not the AI role. Oracle suggests one clearly named auth ID per environment, such as sdf_sbx_deploy and sdf_prod_deploy.

project:deploy has no --authid option: it uses the project’s default auth ID in project.json (account:setup changes it), and the CLI 4.1.0 source skips the production warning. Check the default before each deploy. --dryrun previews without deploying.

Validation runs on the server since CLI 4.0.0 and, since the August 2026 minor release, checks “the schema, referenced files, and mapping between tool definitions and script methods”. For a failure the CLI does not explain, open Customization > SuiteCloud Development > Deployment Audit Trail.

Activate, connect and test

  1. Open Customization > Scripting > Custom Tools (needs SuiteScript (Full)) and expand Acme Credit Tools. Check the Inactive column: if the toolset is inactive, an Administrator selects Activate. Only tools in active toolsets are served.

  2. Use the /all URL: Oracle says the client should call it “to retrieve all available MCP tools”. Copy your account part from Setup > Company > Company Information, Company URLs subtab: a sandbox 123456_SB1 becomes 123456-sb1.

    https://<accountid>.suitetalk.api.netsuite.com/services/mcp/v1/all
    
  3. Already connected with the Standard Tools URL (/suiteapp/com.netsuite.mcpstandardtools)? Replace that connector with one on /all; a second connector would expose the standard tools twice. Anthropic: to change a custom connector, remove it and add it again.

  4. In Claude, go to Customize > Connectors, select + next to Connectors, then Add custom connector (Oracle’s page: Settings > Connectors). Enter a name and the URL, select Add, then Connect, and sign in with the AI role. On Team and Enterprise, an Owner adds connectors.

  5. In ChatGPT (developer mode), first add nullable and an outputSchema, and test whether it wants a string or an object back (Oracle’s ChatGPT sample returns objects). Oracle says each new ChatGPT connection with custom tools needs a new integration record.

  6. Ask “Can we accept a 25,000 order from Acme Corp?”. The tool needs Acme’s internal ID: through /all, Claude can look it up with a standard tool such as ns_runCustomSuiteQL, if your role allows. Otherwise, add a lookup tool like searchCustomer in Oracle’s customer sample.

  7. Find the audit entry in Customization > Scripting > Script Execution Logs. The AI Connector integration record (Setup > Integration > Manage Integrations, Execution Log tab, AI Connector Service (MCP) subtab) shows whether the call reached NetSuite.

After each sandbox refresh, sign in again from the client (Oracle: users “must authorize applications explicitly in the sandbox”) and redeploy, since a refresh overwrites sandbox customizations. Full client steps: How to connect NetSuite to Claude, ChatGPT and other AI clients.

Permissions and safety

Visibility is not access. Oracle: the toolset permissions “determine the visibility of the tool”, while data access and actions “still depend on the user’s NetSuite role and permissions.”

The tool runs as the user. Oracle: tools “can only access data you’re allowed to see when logged in with this role.” Test in a sandbox what your tool returns for a customer outside the role’s subsidiaries.

Annotations are hints to the client. OpenAI treats tools without readOnlyHint as write actions, which need confirmation by default. Claude lets you set each tool to Always allow, Needs approval or Blocked; Anthropic does not say whether it reads the hint.

Oracle documents no NetSuite-side check of annotations: its own sample marks two write tools read-only. Set readOnlyHint to true only for tools that never write.

Guarded writes. Validate every input, touch one record type, return what changed, and give the role Edit on that record type only. Activation is per toolset (“You cannot activate or deactivate individual tools”), so a risky write tool deserves its own toolset.

A write tool adds no approval step. In a 2025 Moss Adams demo, a custom tool’s purchase order went straight to Pending Receipt, with no approval process set up (Moss Adams, AI Connector webcast). Set up approval routing first: Claude’s Needs approval and ChatGPT’s confirmation only ask the person chatting.

Design for what the AI sees. Claude shows none of NetSuite’s cues (sandbox color, current role, pop-ups, mandatory-field markers), so Moss Adams advises building the warnings you rely on into the tool (Moss Adams, safeguards).

When the user only needs to open a record, Moss Adams suggests returning a link instead of its data: Claude then sees the ID, not the record (Moss Adams, links not data). Oracle’s samples do not show how a tool builds that URL: test it in a sandbox.

Audit. The integration record’s execution log keeps calls 21 days in production, 7 in sandbox. Since the August 2026 minor release, in the current NetSuite experience, the System Notes AI Used column tracks “only changes made by custom tools invoked through the NetSuite AI Connector Service”.

Oracle also names “Tool poisoning” and “Rug pull or tool drift” risks: review schema changes like code. More: Governing AI access to NetSuite.

The tool object no longer deploys: migrate to toolset

When What happens
NetSuite 2026.1 toolset replaces tool; execution logs require the new format
September 2026 minor release Tools using tool “can no longer be deployed”, new or updated
NetSuite 2027.1 Tools using tool “will no longer execute”

Before, in Oracle’s sample of November 2025:

<tool scriptid="customtool_suiteql_acp">
  <name>SuiteQL Tools</name>
  <scriptfile>[/SuiteScripts/suiteql_acp.js]</scriptfile>
  <rpcschema>[/SuiteScripts/suiteql_schema_acp.json]</rpcschema>
  <exposeto3rdpartyagents>T</exposeto3rdpartyagents>
</tool>

After, in Oracle’s current sample:

<toolset scriptid="custtoolset_suiteql">
  <name>SuiteQL Tools</name>
  <scriptfile>[/SuiteScripts/toolset/suiteql.js]</scriptfile>
  <rpcschema>[/SuiteScripts/toolset/suiteql_schema.json]</rpcschema>
  <exposetoaiconnector>T</exposetoaiconnector>
</toolset>

Checklist:

  1. Rename the root element tool to toolset.
  2. Change the scriptid prefix customtool_ to custtoolset_ (27 characters maximum) and rename the file to match.
  3. Replace <exposeto3rdpartyagents> with <exposetoaiconnector>.
  4. Add @NScriptType CustomTool to the script header. @NModuleScope Public, in Oracle’s 2025 samples, can stay: the Help Center sample keeps it, the current GitHub samples drop it.
  5. Declare every tool method async.
  6. Remove N/http, N/https, N/llm and N/sftp.
  7. Keep only documented schema properties, and declare SERVERSIDESCRIPTING in manifest.xml.
  8. Deploy to sandbox, activate, call each tool, read the logs, then deploy to production before 2027.1.

What the checklist leaves open (check it in the sandbox):

  • Delete the old customtool_ XML from Objects/: NetSuite refuses to deploy tool objects.
  • The old object stays in the account. For ACPs, Oracle says “You must manually delete objects directly from the NetSuite account”. It does not document where old tool objects are listed: look on the Custom Tools page, where ACP toolsets can be deleted.
  • Until then, the old tool keeps executing (until 2027.1) and the account may serve two tools with the same name. Check the client’s tool list before production.
  • File paths can stay. Oracle’s sample moved its files to /SuiteScripts/toolset/, but nothing requires it, and SDF does not delete the originals of moved files.

Oracle’s migration article is SuiteAnswers 1024036 (login required). AI coding assistants may still generate tool: Oracle’s netsuite-sdf-safe-guide skill keeps a 2025.2 template that it says “deploys to both 2025.2 AND 2026.1 accounts”. See Oracle’s NetSuite agent skills and AI-assisted SuiteScript and SDF development.

Most community repos still use tool; current-format ones are in Community skills, MCP servers and AI tools.

Common problems: deploy and connection errors

Symptom Cause Fix
A 2025 tutorial or GitHub tool will not deploy Old tool object Apply the migration checklist
A deploy fails and the CLI shows little detail The full report stays in NetSuite Customization > SuiteCloud Development > Deployment Audit Trail
“…is not a valid RPC schema JSON file.” Invalid JSON, such as the doubled { in the first schema on Oracle’s code-samples page Fix and validate the JSON
Unsupported property "<property>" at <path> A property outside Oracle’s list Remove it
“…does not export a method with the same name.” Schema name differs from the method Make them identical
“Couldn’t load modules in the custom tool script because it uses unsupported modules.” Unsupported module Remove it
Tool on the Custom Tools page, absent in the client URL not /all, <exposetoaiconnector> missing, toolset inactive, or a listed permission missing from the role Fix, then reconnect the client
“Access denied.” The role cannot open the File Cabinet folder holding the tool Lift the folder restriction
“This tool is not allowed” The session lacks a permission the tool needs Compare the role with <permissions>
No entry in Script Execution Logs Nothing logged on that path; or old format, no @NScriptType CustomTool, a method without async Log on success; confirm the call in the integration record’s Execution Log; apply the 2026.1 requirements

Connection errors: NetSuite AI Connector (MCP) errors.

If you started from Oracle’s samples

  1. ACP paths. The Help Center toolset sample uses a SuiteApp path (/SuiteApps/com.netsuite.tools/...). An ACP uses /SuiteScripts/, as the GitHub sample does.
  2. async. In the GitHub customer.js, createCustomer lacks async.
  3. Correct annotations. customer_schema.json marks createCustomer and updateCustomer "readOnlyHint": true although both save records.
  4. Permissions. The Help Center sample requires Email Template (LIST_EMAILTEMPLATE, FULL) and CRM Lists (ADMI_CRMLIST, VIEW), which hide your tool from roles lacking them.
  5. Raw SuiteQL. runCustomSuiteQL runs any SuiteQL the model sends. Oracle’s SDF agent skill: “Never expose a CustomTool parameter that accepts raw SuiteQL”.

FAQ

My custom tool is deployed in an ACP and shows in NetSuite, but not in Claude. Why?

Tools from an account customization project come through the /services/mcp/v1/all URL, not a /suiteapp/ URL. Then check <exposetoaiconnector>T</exposetoaiconnector>, that the toolset is active, and that your non-Administrator role holds every permission in <permissions>.

How do I test a custom tool before connecting Claude?

Unit-test it with Jest. Oracle’s sample project mocks N/query with @oracle/suitecloud-unit-testing, calls each tool method with await, and runs the tests before every project:deploy (see its suitecloud.config.js). Then test end to end in a sandbox.

I changed the schema and redeployed. Why does Claude still show the old tool?

Neither Oracle nor Anthropic documents when Claude reloads a connector’s tool list. Practitioners report refreshing or reconnecting the connector after permission changes: do the same after a schema change, then check the tool list.

How do I pass sales order lines or other structured data to a custom tool?

Inputs accept only string, number and boolean. Oracle’s SDF agent skill recommends a string parameter holding JSON, parsed with JSON.parse() in the script. Validate the result before using it.

Can a custom tool call Claude, OpenAI or another API from NetSuite?

No. N/http, N/https, N/llm and N/sftp are not supported, and tools cannot invoke Suitelets. For AI inside NetSuite, use N/llm in a server script, which consumes NetSuite AI Units.

Do custom tools consume NetSuite AI Units?

No. Oracle says the AI Connector Service “will not consume AI Units”, and custom tools cannot call N/llm, the module that does. The cost is your AI client plan (what AI on NetSuite costs).

Can I give the AI my account’s schema so it stops inventing field names?

Partly with ns_getSuiteQLMetadata (fields, types, descriptions, joins). A custom tool can add how your team uses each custom record and field. BrokenRubik’s public netsuite-mcp-context-ql repository (no license) shows the idea but uses the old tool object. See Map and document a NetSuite account with AI.

Sources

Oracle Help Center (read September 26, 2026):

Oracle GitHub:

AI clients and third party:

About SeamSet: when the rule you expose to AI also needs a screen, an approval step or a scheduled export, SeamSet builds it from a request in plain words, sandbox first and approved by your NetSuite admin.