ℹ️ Important Update (Aug 18, 2025)
As of this week, it appears that the mcp
scope no longer works with custom NetSuite integration records unless you are using the Claude-specific integration. This may have been an unintended loophole that Oracle has since closed.
What this means: You can still build your own OAuth 2.0 integrations with NetSuite, but if you run into a ScopeMismatch
error, you’ll need to adjust your approach.
For details, see this excellent follow-up article by Michoel Chaikin — Breaking Open NetSuite AI Connector Service: From Claude Pro to Any Chatbot, written shortly after we troubleshot the issue together. His post outlines a working modified approach.
BYOAI MCP NetSuite
Understanding how to connect your AI assistant to NetSuite using Model Context Protocol (MCP)
By: Caleb Moore, Certified SuiteCloud Developer
⚠️ Critical AI Disclaimer
AI systems can and do hallucinate. This guide demonstrates how to integrate AI with NetSuite, but the responses generated by your AI system may contain inaccuracies, false information, or completely fabricated data.
Essential Requirements:
This integration is a starting point - achieving reliable, production-ready AI responses requires ongoing validation, testing, and refinement until you reach acceptable confidence levels for your use case.
⚠️ Rapidly Evolving Technology: This document reflects the current state of NetSuite MCP and AI integration as of the date above. Given the fast-paced nature of AI technology and NetSuite updates, portions of this guide may become outdated quickly. Always verify current NetSuite documentation and AI service capabilities.
This is a conceptual guide that explains the key components and architecture needed to connect your AI assistant to NetSuite MCP. It’s not a copy/paste tutorial, but rather helps you understand what you need to build and how the pieces fit together.
This concept was proven using: Google Gemini AI + React-based chatbot interface
However, there are many ways to leverage NetSuite’s AI Connector: You could use Claude, GPT-4, Azure OpenAI, or any other AI service. The architecture principles remain the same regardless of your AI choice.
Current State: While this integration concept has been proven to work, it isn’t perfect and would still benefit from further refinement. Expect some bugs and edge cases that need addressing.
This guide explains the architecture and key components needed to connect your AI assistant to NetSuite using Model Context Protocol (MCP). The goal is to create a system where users can ask natural language questions and get real-time data from NetSuite without needing to know SuiteQL or navigate complex interfaces.
How to architect an AI-powered system that bridges natural language queries with NetSuite’s MCP tools, enabling conversational data access to your business data.
👤 User Question → 🧠 AI Analysis → 🔧 MCP Execution → 💬 AI Interpretation
Example Flow:
SELECT COUNT(*) FROM customer
Feature | Description |
---|---|
🧠 AI Query Analysis | Your AI analyzes user questions and selects the right MCP tool with proper parameters |
🔧 MCP Tool Execution | Executes the selected tool against NetSuite and retrieves raw data |
💬 AI Response Interpretation | Your AI interprets the NetSuite results and provides conversational answers |
🛡️ Smart Fallbacks | Intelligent parsing when AI interpretation fails |
Note: The examples in this guide use Google Gemini AI + React + Node.js, but this architecture is flexible and can be adapted to your preferred AI service, frontend framework, or backend technology.
Before you can connect your AI to NetSuite, you need to set up the MCP (Model Context Protocol) infrastructure on the NetSuite side. This involves installing the MCP Tools SuiteApp and configuring the necessary permissions.
The MCP Tools SuiteApp provides the interface between your AI and NetSuite data. You’ll need to:
⚠️ Important
Administrators are not allowed to work with MCP - you must create a custom role with specific permissions.
You’ll need to create a custom role with these key permissions:
This role will be assigned to the user account that your AI integration will use to connect to NetSuite.
Assign the custom MCP role to the user account that will be used for your AI integration. This user will authenticate with NetSuite on behalf of your AI system.
Create an OAuth 2.0 integration record that will allow your AI system to authenticate with NetSuite. Key configuration points:
mcp
scope is handled in the OAuth authorization call, not in the NetSuite integration record⚠️ Important: Disable These Features
Make sure these features are disabled in your integration record:
This integration record will provide you with a Client ID that your AI system will use during the OAuth flow.
Your AI integration system will need several core components to function properly. Here’s what you’ll need to build or integrate:
You’ll need a backend server that handles:
Your user interface should provide:
The core intelligence layer includes:
This is where the magic happens! Your AI needs to understand user questions, select the right MCP tools, and interpret the results conversationally. Here’s how the AI integration works:
Your AI (Gemini in our case) needs to:
💡 Example AI Analysis
User: "How many customers do we have?"
AI Analysis: Use `runCustomSuiteQL` with `SELECT COUNT(*) FROM customer`
Once your AI determines the right tool and parameters, you need to:
🔧 MCP Request Format
Your requests to NetSuite's MCP server should follow the JSON-RPC 2.0 specification:
{
"jsonrpc": "2.0",
"id": "unique_request_id",
"method": "tools/call",
"params": {
"name": "toolName",
"arguments": { ... }
}
}
This is the key component that makes your AI conversational! Instead of returning raw JSON data, your AI needs to:
💬 Example Response Transformation
Raw NetSuite Response: `{"expr1": 4214}`
AI Interpretation: "You have 4,214 customers in your system."
app.post("/netsuite/mcp/tools", async (req, res) => {
try {
const accountId = req.headers["x-netsuite-account"];
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(401).json({
error: "Missing Authorization header",
});
}
const mcpUrl = `https://${accountId}.suitetalk.api.netsuite.com/services/mcp/v1/all`;
const requestBody = {
jsonrpc: "2.0",
id: "2",
method: "tools/list",
params: {},
};
const response = await fetch(mcpUrl, {
method: "POST",
headers: {
Authorization: authorization,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (response.ok) {
const data = await response.json();
res.json({
success: true,
message: "MCP tools fetched successfully",
status: response.status,
data: data,
url: mcpUrl,
});
} else {
const errorText = await response.text();
res.status(response.status).json({
success: false,
message: "MCP tools fetch failed",
status: response.status,
error: errorText,
url: mcpUrl,
});
}
} catch (error) {
console.error("MCP tools endpoint error:", error);
res.status(500).json({
error: "MCP tools fetch failed",
details: error.message,
});
}
});
app.post("/netsuite/mcp/execute", async (req, res) => {
try {
const { jsonrpc, id, method, params } = req.body;
const accountId = req.headers["x-netsuite-account"];
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(401).json({
error: "Missing Authorization header",
});
}
if (!jsonrpc || !id || !method) {
return res.status(400).json({
error: "Missing required parameters: jsonrpc, id, method",
});
}
const mcpUrl = `https://${accountId}.suitetalk.api.netsuite.com/services/mcp/v1/all`;
const requestBody = {
jsonrpc,
id,
method,
params: params || {},
};
const response = await fetch(mcpUrl, {
method: "POST",
headers: {
Authorization: authorization,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (response.ok) {
const data = await response.json();
if (data.error) {
return res.status(400).json({
success: false,
message: "MCP tool execution failed",
status: response.status,
error: data.error,
url: mcpUrl,
});
}
res.json({
success: true,
message: "MCP tool executed successfully",
status: response.status,
data: data,
url: mcpUrl,
});
} else {
const errorText = await response.text();
let errorDetails = errorText;
try {
const errorJson = JSON.parse(errorText);
if (errorJson.error) {
errorDetails = errorJson.error;
}
} catch (e) {
// If not JSON, use the raw text
}
res.status(response.status).json({
success: false,
message: "MCP tool execution failed",
status: response.status,
error: errorDetails,
url: mcpUrl,
});
}
} catch (error) {
console.error("MCP tool execution endpoint error:", error);
res.status(500).json({
error: "MCP tool execution failed",
details: error.message,
});
}
});
app.listen(PORT, () => {
console.log(`Backend server running on port ${PORT}`);
console.log(`CORS enabled for origin: http://localhost:3000`);
});
// Generate PKCE code verifier
export function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// Generate PKCE code challenge
export async function generateCodeChallenge(codeVerifier) {
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(codeVerifier)
);
return base64URLEncode(new Uint8Array(hash));
}
// Base64 URL encoding
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
The main React component handles:
Feature | Description |
---|---|
🔐 OAuth 2.0 PKCE Flow | Secure authentication with NetSuite using industry-standard OAuth 2.0 with PKCE |
🔍 MCP Tool Discovery | Automatically fetches available tools from NetSuite’s MCP server |
🤖 AI-Powered Responses | Uses Google Gemini to interpret NetSuite data naturally |
🛡️ Smart Fallbacks | Handles errors gracefully with intelligent parsing |
const [netsuiteConfig, setNetsuiteConfig] = useState({
accountId: "",
consumerKey: "",
});
const [accessToken, setAccessToken] = useState(null);
const [refreshToken, setRefreshToken] = useState(null);
const [mcpTools, setMcpTools] = useState([]);
Before executing any MCP tools, the AI analyzes the user’s question to determine the right tool and parameters:
const analyzeUserQuery = async (userInput) => {
try {
const genAI = new GoogleGenerativeAI(process.env.REACT_APP_GEMINI_API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const analysisPrompt = `You are an AI assistant that helps users interact with NetSuite data through MCP tools.
Available MCP tools:
${mcpTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n")}
User question: "${userInput}"
Analyze this question and determine:
1. Which MCP tool should be used
2. What parameters to pass to the tool
3. Your confidence level (0-100)
Respond with JSON in this exact format:
{
"confidence_score": 85,
"action": "execute_tool",
"tool_name": "runCustomSuiteQL",
"tool_params": {
"sqlQuery": "SELECT COUNT(*) FROM customer",
"description": "Count all customers"
},
"reason": "User wants to count customers, so I'll use runCustomSuiteQL with a COUNT query"
}
If you're not confident or need clarification, use:
{
"confidence_score": 45,
"action": "ask_clarification",
"reason": "The request is ambiguous. Please specify what type of customer information you need."
}`;
const result = await model.generateContent(analysisPrompt);
const response = await result.response;
const responseText = response.text();
// Extract JSON from the response
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error("Failed to parse AI analysis response");
} catch (error) {
console.error("AI query analysis failed:", error);
return {
confidence_score: 0,
action: "ask_clarification",
reason:
"I'm having trouble understanding your request. Please try rephrasing it.",
};
}
};
const executeMcpTool = async (toolName, params) => {
try {
let currentAccessToken = accessToken;
if (!currentAccessToken) {
currentAccessToken = localStorage.getItem("netsuite_access_token");
if (currentAccessToken) {
setAccessToken(currentAccessToken);
}
}
if (!currentAccessToken) {
throw new Error(
"No access token available. Please authorize with NetSuite first."
);
}
const response = await fetch("http://localhost:3001/netsuite/mcp/execute", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${currentAccessToken}`,
"X-NetSuite-Account": netsuiteConfig.accountId,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: Date.now().toString(),
method: "tools/call",
params: {
name: toolName,
arguments: params,
},
}),
});
if (response.status === 401) {
// Token expired, try to refresh automatically
try {
const newToken = await refreshAccessToken();
// Retry with new token
const retryResponse = await fetch(
"http://localhost:3001/netsuite/mcp/execute",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${newToken}`,
"X-NetSuite-Account": netsuiteConfig.accountId,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: Date.now().toString(),
method: "tools/call",
params: {
name: toolName,
arguments: params,
},
}),
}
);
if (!retryResponse.ok) {
const errorData = await retryResponse.json();
throw new Error(
`MCP tool execution error after token refresh: ${
retryResponse.status
} - ${errorData.error || "Unknown error"}`
);
}
const result = await retryResponse.json();
return result;
} catch (refreshError) {
throw new Error(
`Authentication failed: ${refreshError.message}. Please re-authorize with NetSuite.`
);
}
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`MCP tool execution error: ${response.status} - ${
errorData.error || "Unknown error"
}`
);
}
const result = await response.json();
return result;
} catch (error) {
console.error("MCP tool execution error:", error);
throw error;
}
};
const initializeOAuth = async (e) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
try {
if (!netsuiteConfig.accountId || !netsuiteConfig.consumerKey) {
throw new Error("Missing required credentials");
}
setIsInitializing(true);
// Generate PKCE code verifier and challenge
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Generate state parameter
const state =
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2);
// Store PKCE code verifier and state
localStorage.setItem("code_verifier", codeVerifier);
localStorage.setItem("oauth_state", state);
// Build authorization URL with MCP scope only
const authUrl = `https://${
netsuiteConfig.accountId
}.app.netsuite.com/app/login/oauth2/authorize.nl?response_type=code&client_id=${
netsuiteConfig.consumerKey
}&redirect_uri=${encodeURIComponent(
"http://localhost:3000"
)}&scope=mcp&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}`;
// Redirect to NetSuite authorization
window.location.href = authUrl;
} catch (error) {
console.error("OAuth initialization failed:", error);
setIsInitializing(false);
alert(`OAuth initialization failed: ${error.message}`);
}
};
This is the key component that makes the bot intelligent and conversational. Instead of returning raw JSON data, the AI interprets NetSuite results and provides natural language answers.
// AI interpretation prompt for Gemini
const interpretationPrompt = `You are a helpful NetSuite assistant. The user asked: "${inputText}"
I successfully retrieved data from NetSuite using the MCP tool "${toolName}". The tool executed successfully and returned data. Here is the complete response:
${JSON.stringify(toolResult, null, 2)}
CRITICAL: The MCP tool executed SUCCESSFULLY. Do NOT say there was an error or that the system doesn't recognize terms. The data IS available in this response.
Your task:
1. Look through this entire response structure to find the actual data
2. The data is likely nested in the response structure
3. Answer the user's original question based on the data you find
4. Give a natural, conversational answer
REQUIREMENTS:
- The tool executed successfully, so the data IS available
- Answer the question directly and briefly
- Don't mention technical details like "MCP tool", "NetSuite", or show raw data
- Don't explain HOW you found the information
- Just give the answer a human would give
- If it's a count, format numbers nicely (e.g., "4,214" not "4214")
- Keep it conversational and helpful
IMPORTANT: Look for success indicators like "status: 200" and "success: true" - these mean the data retrieval was successful.`;
// Use Gemini AI to interpret the results
const genAI = new GoogleGenerativeAI(process.env.REACT_APP_GEMINI_API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
try {
const result = await model.generateContent(interpretationPrompt);
const response = await result.response;
const responseText = response.text();
// Clean up the response
return responseText.replace(/^```\w*\n?|\n?```$/g, "").trim();
} catch (interpretationError) {
console.error("AI interpretation failed:", interpretationError);
// Fallback to intelligent parsing if AI fails
return extractAnswerFromData(toolResult, inputText);
}
When AI interpretation fails, you need intelligent fallback mechanisms to ensure users still get helpful responses. This is crucial for production reliability.
🛡️ Why Fallbacks Are Essential
AI interpretation can fail for many reasons:
Your fallback strategy should include:
💡 Example Fallback Approach
Instead of hardcoding specific field names like `expr1`, design your fallback to:
Here’s how the complete flow works when a user asks a question:
const handleSendMessage = async (inputText) => {
try {
// 1. AI determines the right MCP tool and parameters
const aiAnalysis = await analyzeUserQuery(inputText);
if (aiAnalysis.action === "execute_tool") {
// 2. Execute the MCP tool against NetSuite
const toolResult = await executeMcpTool(
aiAnalysis.tool_name,
aiAnalysis.tool_params
);
// 3. Use AI to interpret the results conversationally
const responseText = await interpretNetSuiteResults(
toolResult,
inputText,
aiAnalysis.tool_name
);
// 4. Add the response to the chat
addMessage("bot", responseText);
} else {
// Handle clarification requests
addMessage("bot", aiAnalysis.reason);
}
} catch (error) {
console.error("Message handling failed:", error);
addMessage("bot", `I encountered an error: ${error.message}`);
}
};
🔧 Implementation Approach
Remember: This guide shows one way to implement the NetSuite MCP integration. You can adapt these concepts to your preferred technology stack and development approach.
You’ll need to set up a development environment that supports:
Test your integration systematically:
Once your system is set up, the basic workflow is:
You’ll need to configure:
Verify your integration works by:
“MCP tools not available”
“Authentication failed”
http://localhost:3000
“Gemini API key not configured”
.env
filePort conflicts
The application includes comprehensive console logging. Check the browser console and terminal for:
npm run build
Yes, you CAN connect your AI assistant to NetSuite using MCP! While it may have some bugs and challenges (as any integration does), the core architecture works and provides a powerful way to give users conversational access to their NetSuite data.
Using Gemini AI, we successfully created a system that:
Feature | Description |
---|---|
🔒 Secure | OAuth 2.0 PKCE authentication with NetSuite |
🧠 Intelligent | AI-powered query analysis and response generation |
⚡ Real-time | Live data access through NetSuite’s MCP server |
🔧 Extensible | Easy to add new MCP tools and capabilities |
The integration works, it’s powerful, and it opens up new possibilities for how users interact with their NetSuite data. While there are challenges to overcome, the foundation is solid and the potential is enormous.
This guide represents the current state of NetSuite MCP AI integration as of August 15, 2025. Given the rapidly evolving nature of this technology, always verify current NetSuite documentation and AI service capabilities.
This guide is open source and we welcome contributions! The repository is hosted at https://github.com/devszilla/netsuite-mcp-ai-guide.
Found an error or outdated information?
Want to add new content?
Have a different approach?
git clone https://github.com/devszilla/netsuite-mcp-ai-guide.git
cd netsuite-mcp-ai-guide
# Make your changes
# Submit a pull request