Skip to content

Connect AI Agents to NetSuite & SAP via MCP: The 2026 Architecture Guide

A code-first architectural blueprint for connecting AI agents to Oracle NetSuite and SAP using dynamic, zero-retention Model Context Protocol (MCP) servers.

Roopendra Talekar Roopendra Talekar · · 13 min read
Connect AI Agents to NetSuite & SAP via MCP: The 2026 Architecture Guide

If you are trying to figure out the best way to connect AI agents to Oracle NetSuite and SAP with runnable MCP tutorials, the honest architectural answer is this: don't hand-roll integrations against SuiteTalk, SuiteQL, or OData v4. Building point-to-point LangChain connectors for legacy ERPs fails at scale. Instead, engineering teams must wrap these legacy surfaces behind a managed Model Context Protocol (MCP) server that generates tools dynamically from documentation, filters them by method and tag, and passes ERP responses through without caching.

That architecture gives your agent a clean JSON schema to reason over, and gives your security team a story they can actually defend.

This guide provides the code-first architectural blueprint for senior engineering leads and product managers shipping multi-tenant AI features against customers' NetSuite and SAP instances. We will walk through why standard agentic connectors collapse against ERP surfaces, how to expose SuiteQL and OData resources as MCP tools without hallucination risk, and give you runnable code you can paste into Claude, ChatGPT, or a custom agent runtime today.

Why Connecting AI Agents to ERPs Breaks Standard Frameworks

Large Language Models (LLMs) are highly deterministic when fed predictable, flat JSON schemas. They fail spectacularly when handed deeply nested, highly customized XML, SOAP envelopes, or complex OData structures with cryptic field names. ERPs were architected in an era of internal ID references that only make sense if you also know the account's chart of accounts, subsidiary structure, and custom fields. This architectural mismatch is where most agentic workflows die.

The macro data backs this up:

  • Gartner (2024) found that roughly 60% of ERP implementations hit data quality problems and 70% of ERP projects miss their objectives - meaning the substrate your agent is reading from is already noisy before an LLM ever touches it.
  • A 2026 industry survey reported by DreamFactory shows 95% of IT leaders cite integration issues as the primary impediment to AI adoption, with the average enterprise running 897 applications and only 28% of them integrated.
  • Gartner further projects that by 2027, more than 70% of recently implemented ERP initiatives will fail to fully meet their original business case goals, largely because of integration and data-model debt.
  • SAP's own architecture guidance explicitly notes that wiring an MCP server directly to raw SAP transactional APIs without semantic enrichment leads to poor entity discovery and a significant risk of the agent executing incorrect business transactions.

That last point is the one engineering leaders underestimate. Giving an LLM a tool called POST /journalEntry with a 400-field OpenAPI blob is not "AI-ready." It is a loaded gun pointed at your customer's general ledger.

Warning

The real failure mode isn't a 500 error - it's a semantically valid but business-wrong transaction. An agent that successfully posts an invoice against the wrong subsidiary in NetSuite will not throw an exception. It will look like a success in your logs and a fire drill in your customer's finance team.

The traditional approach of writing hardcoded API wrappers for every NetSuite or SAP endpoint does not scale. To solve this, you need an architecture that normalizes these legacy payloads into AI-ready schemas and exposes them securely via MCP.

The Best Way to Connect AI Agents to Oracle NetSuite

Writing custom API connectors for NetSuite is an engineering nightmare. NetSuite is the canonical example of API fragmentation. A single AI workflow that reads open invoices, updates a customer record, and pulls a P&L will touch three distinct API surfaces:

Surface Use Case Pain Point
SuiteQL (REST) Bulk reads, custom queries, joins No native pagination cursors, 5,000-row hard cap per query
REST Record Service CRUD on standard records Inconsistent field naming vs SuiteQL, subsidiary-scoped
SuiteTalk SOAP Tax rates, some legacy operations XML envelopes, WSDL versioning, TBA auth
SuiteScript (RESTlets) Dynamic metadata, PDF generation Requires deployed script, per-account

Oracle now offers a first-party NetSuite AI Connector Service with a role-based MCP standard tools SuiteApp. If you are building an internal tool for your own finance team on a single NetSuite instance, start there. But if you are shipping a B2B SaaS product where every customer has their own NetSuite account, custom fields, and subsidiary structure, single-tenant SuiteApps don't scale. You need a multi-tenant platform architecture to handle the punishing realities of NetSuite's legacy authentication models and polymorphic resource routing.

Dynamic Tool Generation

The scalable solution is dynamic, documentation-driven tool generation. Rather than hand-coding a create_invoice tool for every customer, the MCP server should derive tools directly from the integration's resource definitions and documentation records. A tool only appears in the MCP server if it has a corresponding documentation entry. This acts as both a quality gate (undocumented endpoints don't leak to the LLM) and a curation mechanism (finance-critical endpoints get explicit schemas, edge cases don't).

When an MCP client requests available tools, the server executes the following sequence:

  1. Fetch Documentation: The system fetches documentation records, merging integration-level defaults with environment-specific overrides. This allows you to customize tool descriptions for specific tenants without affecting others.
  2. Iterate Resources: The system iterates over every resource and method. If a documentation record exists, it proceeds.
  3. Generate Identifiers: Tool names are generated as descriptive snake_case strings using the integration label and resource name (e.g., list_all_oracle_net_suite_invoices or get_single_net_suite_contact_by_id).
  4. Build Schemas: Query and body schemas are extracted from the documentation YAML. The system parses this into JSON Schema, injecting properties like id for individual methods, or limit and next_cursor for list methods.
  5. Assemble Tool: The final tool definition is returned to the LLM.

The LLM never sees SuiteQL. It sees a clean, typed tool with a snake_case name that maps directly to intent. If you are evaluating platforms for multi-tenant SaaS, check out our guide on the best MCP server for Oracle NetSuite in 2026.

How to Connect AI Agents to SAP ERP via MCP

SAP S/4HANA presents a different but equally complex API surface. SAP is where the semantic-enrichment argument becomes non-negotiable. S/4HANA exposes thousands of OData v2 and v4 services, plus BAPIs, RFCs, and IDocs, with field names like KUNNR, BUKRS, and MATNR.

An LLM cannot intuit the difference between a BAPI_SALESORDER_CREATEFROMDAT2 endpoint and a standard OData POST without semantic context. An agent that gets raw metadata for API_SALES_ORDER_SRV will confidently call the wrong entity set the first time it sees A_SalesOrderItem next to A_SalesOrderScheduleLine.

To prevent the agent from hallucinating destructive transactions, the MCP server must support granular method and tag filtering. Two mechanisms make this workable in a multi-tenant MCP server:

Tag-Based Tool Grouping

Resources are annotated with functional tags at the integration config level to organize and filter tools by functional area:

{
  "tool_tags": {
    "sales_orders": ["o2c", "sales"],
    "invoices": ["o2c", "finance"],
    "journal_entries": ["finance", "gl"],
    "purchase_orders": ["p2p", "procurement"],
    "vendors": ["p2p", "master_data"]
  }
}

When you provision an MCP server for a customer's finance agent, you scope it: config: { tags: ["finance", "gl"] }. The dynamic generation logic applies this filter at the documentation-fetching stage. The agent literally cannot see procurement tools. That is your blast radius control.

Method-Level Filtering

On top of tags, MCP server tokens support method filters that map to operation categories:

Filter Matches
"read" get, list
"write" create, update, delete
"custom" Anything else (e.g., search, post_journal, run_report)
Exact name "list", "create", etc.

Filters compose. methods: ["read", "custom"] gives an agent read-only access plus curated business actions like run_report, but blocks raw create/update/delete. Every filter combination is validated at server-creation time so you can't accidentally ship an empty tool list. Since tools without documentation are skipped entirely, the agent is physically prevented from executing a write operation or accessing out-of-scope data.

Read more about handling these specific engineering challenges in our guide to connecting AI agents to NetSuite and SAP Concur via MCP servers.

Runnable MCP Tutorials: Code-First Architecture for ERPs

Let's look at a concrete implementation. Here is the end-to-end flow. You create one MCP server per customer's connected ERP account, then hand the URL to Claude, ChatGPT, or your own agent runtime.

sequenceDiagram
    participant Agent as AI Agent (Claude / ChatGPT / Custom)
    participant MCP as MCP Endpoint (Edge Router)
    participant Docs as Documentation-Driven Tool Generator
    participant ERP as "Upstream ERP (NetSuite / SAP)"
    Agent->>MCP: POST /mcp/{token} initialize
    MCP-->>Agent: capabilities + server info
    Agent->>MCP: tools/list
    MCP->>Docs: getTools(account, methods, tags)
    Docs-->>MCP: filtered tool array with JSON Schema
    MCP-->>Agent: tools/list response
    Agent->>MCP: tools/call list_all_net_suite_invoices
    MCP->>ERP: proxy authenticated request (OAuth / TBA)
    ERP-->>MCP: response payload
    MCP-->>Agent: normalized result + next_cursor

Step 1: Provision an MCP Server for the Customer's ERP Account

After your customer connects their NetSuite or SAP account through your linking flow, create a scoped MCP server. You can do this via cURL or any standard HTTP client. We will restrict this server to read-only operations for finance tags.

Via cURL:

curl -X POST https://api.truto.one/integrated-account/ia_abc123/mcp \
  -H "Authorization: Bearer $TRUTO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Finance Agent - NetSuite",
    "config": {
      "methods": ["read", "custom"],
      "tags": ["finance", "gl"],
      "require_api_token_auth": true
    },
    "expires_at": "2026-12-31T23:59:59Z"
  }'

Via Node.js (TypeScript):

async function createMcpServer(accountId: string, apiToken: string) {
  const response = await fetch(`https://api.truto.one/integrated-account/${accountId}/mcp`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: 'Read-Only Finance Agent',
      config: {
        methods: ['read'],       // Only allow 'get' and 'list'
        tags: ['finance']        // Only expose finance-tagged resources
      },
      expires_at: '2026-12-31T23:59:59Z' // Optional TTL
    })
  });
 
  const data = await response.json();
  console.log('MCP Server URL:', data.url);
  return data.url;
}

The response returns a self-contained URL (e.g., https://api.truto.one/mcp/a1b2c3d4e5f6...). The token cryptographically encodes the integrated account, the allowed methods, the tag scope, and the expiry. No client-side config beyond the URL is required.

Step 2: Wire the URL into Your Agent Runtime

Once you have the URL, you can connect any MCP-compatible client.

Claude Desktop or Web: Go to Settings → Connectors → Add custom connector → paste the URL. Custom connectors via remote MCP are available on Free, Pro, Max, Team, and Enterprise plans.

ChatGPT: Go to Settings → Apps → Advanced settings → enable Developer mode → add a custom connector with the URL. Developer Mode is available on Pro, Plus, Business, Enterprise, and Education accounts.

Custom Agent (Python via the mcp SDK):

import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
 
MCP_URL = "https://api.truto.one/mcp/a1b2c3d4e5f6..."
HEADERS = {"Authorization": f"Bearer {TRUTO_API_TOKEN}"}  # if require_api_token_auth=true
 
async def run():
    async with streamablehttp_client(MCP_URL, headers=HEADERS) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
 
            # Fetch available tools to verify the semantic layer
            tools = await session.list_tools()
            for t in tools.tools:
                print(t.name, "-", t.description[:80])
 
asyncio.run(run())

Custom Agent (Node.js via @modelcontextprotocol/sdk):

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
 
async function initializeAgent(mcpUrl: string) {
  // Initialize the transport using the self-contained URL
  const transport = new SSEClientTransport(new URL(mcpUrl));
  
  const client = new Client({
    name: 'finance-agent-client',
    version: '1.0.0',
  }, {
    capabilities: { tools: {} }
  });
 
  await client.connect(transport);
  
  const tools = await client.listTools();
  console.log('Available ERP Tools:', tools.tools.map(t => t.name));
  
  return client;
}

The agent gets tool names like list_all_net_suite_invoices, get_single_net_suite_invoice_by_id, and create_a_net_suite_journal_entry (only if write was allowed). Each carries a JSON Schema with typed properties, required fields, and, for list methods, limit and next_cursor parameters injected automatically.

Step 3: Execute the Tool Call and Chain Workflows

When the LLM decides to fetch data, it issues a tool call. The MCP router handles the JSON-RPC 2.0 protocol, splits the arguments into query and body parameters based on the schema, and proxies the request to the ERP.

A typical accounts-receivable agent loop looks like this in Python:

# 1. Find overdue invoices
invoices = await session.call_tool(
    "list_all_net_suite_invoices",
    arguments={"status": "open", "due_date_before": "2026-08-01", "limit": "100"},
)
 
# 2. For each invoice, fetch the contact details
for inv in parse(invoices):
    contact = await session.call_tool(
        "get_single_net_suite_contact_by_id",
        arguments={"id": inv["contact_id"]},
    )
    # 3. Let the LLM draft a dunning email using inv + contact
    # 4. Optionally log an activity via a custom method tool

Because the tool schemas are normalized into unified accounting entities (Invoice, Contact, Payment, Account), the exact same loop works against SAP with only the connector token swapped. That is the massive payoff of the unified data model. For a broader look at orchestration, review our code-first architecture tutorial for ERP integration.

Handling Enterprise Rate Limits and Pagination in MCP

ERP APIs are notorious for brutal concurrency limits. NetSuite strictly enforces concurrent request limits based on the customer's licensing tier. SAP gateways often throttle aggressive polling. This is where most "unified API" pitches get dishonest. So let's be explicit.

Info

Factual Note on Rate Limits: A production-grade MCP server should not silently absorb, throttle, or infinitely retry rate limit errors on your behalf.

When an upstream API like NetSuite or SAP returns an HTTP 429 Too Many Requests, the platform must pass that error directly back to the caller. The MCP server normalizes the upstream rate limit information into standardized headers per the IETF specification:

  • ratelimit-limit: The maximum number of requests permitted in the current window.
  • ratelimit-remaining: The number of requests remaining in the current window.
  • ratelimit-reset: The time at which the rate limit window resets.

Your agent runtime - or the retry wrapper around your MCP client - is responsible for reading these headers and implementing exponential backoff. Here is the pattern that actually works in production:

import time, random
 
async def call_with_backoff(session, tool, args, max_retries=5):
    for attempt in range(max_retries):
        result = await session.call_tool(tool, arguments=args)
        if not result.isError:
            return result
        
        # Parse ratelimit-reset from the error payload if present
        reset = extract_reset_seconds(result) or (2 ** attempt)
        jitter = random.uniform(0, 0.5)
        time.sleep(reset + jitter)
        
    raise RuntimeError(f"{tool} exhausted retries")

Why pass 429s through instead of hiding them? Because ERP rate-limit windows matter to your agent's planning. NetSuite's concurrency governance is per-account, not per-integration - if you silently retry at the proxy layer, you starve your customer's other integrations and cause connection timeouts that break agent reasoning loops. The caller is the only layer that has the full picture of what other work is queued.

Handling Pagination

For list operations, dynamic tool generation injects a next_cursor property into the JSON Schema. The description explicitly instructs the LLM: "Always send back exactly the cursor value you received without decoding, modifying, or parsing it."

That one line of prompt-in-schema engineering eliminates the most common pagination bug in agentic workflows: models that helpfully "parse" opaque cursors and break them. The agent calls the tool, receives a next_cursor in the response payload, and passes it back verbatim on the next call. No cursor decoding, no offset math, no SuiteQL ROWNUM gymnastics.

Zero Data Retention and Multi-Tenant Security

ERPs contain the most sensitive financial and operational data in a business. Payroll figures, customer PII, revenue by segment, unposted journal entries. Caching this data in a middleware database introduces massive compliance and security risks.

The correct default is zero data retention: the MCP server acts as a pass-through proxy, holding nothing beyond the request lifecycle. That has concrete architectural implications:

  1. Self-Contained Cryptographic URLs: The architecture relies on self-contained MCP server URLs. When an integrated account is connected, the system generates a secure token that encodes the account ID, environment, allowed methods, and expiration time.
  2. Hashed Storage: This token is hashed with a signing key and stored in a low-latency key-value store. The plaintext token exists only in the URL you handed the client.
  3. Dynamic Generation (No Stale Schemas): Tools are generated on every tools/list and tools/call request, never cached. If you update a documentation record to fix a field description, the next agent call sees the fix immediately.
  4. Zero Caching Execution: When the agent executes a tool call, the MCP router proxies the request directly to the ERP, normalizes the response, and streams it back to the agent entirely in memory. No warehouse, no vector store, no side-channel logging of business data is ever written to disk.
  5. Automated Expiration Cleanup: MCP servers can be created with a time-to-live via an expires_at field. Expiration is enforced at multiple levels: the key-value store utilizes built-in TTL expiration, a scheduled background alarm fires at the expiration time to clean up the relational database record, and validation constraints prevent creating immediately-expired servers.
  6. Dual-Layer Authentication: For higher-security scenarios, you can enable require_api_token_auth: true. When enabled, the client must provide a valid API token in the Authorization header alongside the MCP URL, ensuring only authenticated team members can execute tools even if the URL leaks.

The following diagram illustrates how the MCP router proxies the request while maintaining zero data retention:

sequenceDiagram
  participant Agent as "LLM Agent"
  participant Router as "MCP Router (Edge)"
  participant KV as "Key-Value Store"
  participant ERP as "NetSuite / SAP"

  Agent->>Router: POST /mcp/{token} (tools/call)
  Router->>KV: Validate cryptographic token
  KV-->>Router: Token valid (Account ID, Filters)
  Router->>Router: Extract query & body from arguments
  Router->>ERP: Proxy request (SuiteQL/OData)
  ERP-->>Router: Raw ERP Response
  Router->>Router: Normalize payload
  Router-->>Agent: JSON-RPC Result (In-Memory, Streamed)
Tip

Trade-off worth being honest about: zero data retention means you cannot serve agent queries offline, and every tool call incurs the upstream ERP's latency. For finance workloads where correctness beats speed, this is the right trade. For high-QPS analytics agents, you may want a purpose-built read cache with explicit TTLs on top of the MCP layer.

For a deeper dive into this security model, review our zero data retention AI agent architecture guide.

Strategic Next Steps for Engineering Teams

Connecting AI agents to legacy ERP systems is not a prompt engineering problem; it is an infrastructure problem. The competitive landscape for ERP-to-agent connectivity in 2026 is fragmented. Oracle's first-party NetSuite AI Connector Service is the right answer for single-tenant internal tools. Redwood's RunMyJobs targets governed execution across SAP and Oracle. Vendors like Apichap and Apideck are pitching MCP-native coverage for specific functional areas.

However, if you are shipping a multi-tenant B2B SaaS product where every customer has a differently-customized NetSuite or SAP instance, the architectural criteria that matter are:

  • Dynamic, documentation-driven tool generation so you aren't hand-coding API wrappers per customer.
  • Method and tag filtering so a finance agent can't touch procurement, and a read-only agent can't post journal entries.
  • Standards-compliant rate-limit surfacing so your agent runtime can back off intelligently without starving connections.
  • Zero data retention so ERP payloads never persist outside the request lifecycle.
  • Self-contained, revocable, expirable MCP URLs so provisioning and de-provisioning is a single API call.

Stop writing custom SuiteQL queries and OData wrappers for every new LLM feature. Implement a standardized MCP layer that handles the authentication, normalizes the schemas, applies semantic enrichment, and gets out of the way.

FAQ

What is the best way to connect AI agents to Oracle NetSuite?
For single-tenant internal use, start with Oracle's first-party NetSuite AI Connector Service. For multi-tenant B2B SaaS, use a managed MCP server that dynamically generates tools from documentation, filters them by method and tag, and passes ERP responses through without caching. This abstracts away SuiteQL, REST, and SuiteTalk SOAP behind a single JSON-schema tool surface.
How do you connect AI agents to SAP ERP via MCP?
Never expose raw OData services directly. SAP's own architecture guidance warns this leads to poor entity discovery and incorrect business transactions. Instead, use an MCP server that applies semantic enrichment, tag-based tool grouping (e.g., 'o2c', 'finance', 'p2p'), and method filtering to give the agent only the curated tools it needs for a specific workflow.
How do you handle NetSuite API rate limits with AI agents?
Pass HTTP 429 errors from upstream ERPs straight to the caller and normalize upstream rate limit info into standardized IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset). The agent runtime or client wrapper is responsible for exponential backoff, because only the caller has visibility into the full queue of pending work against the customer's ERP account.
How does zero data retention work for MCP servers connected to ERPs?
The MCP server acts as a pass-through proxy. Tools are generated on every tools/list and tools/call request rather than cached, ERP response payloads are streamed back to the caller without storage, and MCP tokens are hashed before storage. Setting expires_at causes the server to be cleaned up at multiple layers when the TTL passes.
Can MCP servers prevent agents from executing unauthorized ERP transactions?
Yes. When you create the MCP server, pass config.methods (e.g., ['read'] for read-only, or ['read', 'custom'] to allow curated business actions but block raw create/update/delete) and config.tags (e.g., ['finance'] to hide procurement tools). Both filters are validated at creation time to ensure at least one tool matches and restrict the agent strictly to specific operational domains.

More from our Blog