NexWave MCP
Allows connecting to an ERPNext site and provides read-only tools for viewing the current user, companies, customers, items, sales orders, and allowlisted sales and purchase documents.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
NexWave MCP
NexWave MCP is a small, self-hosted gateway that connects AI clients such as ChatGPT, Claude, and Codex to a NexWave or ERPNext site.
It uses standard MCP with OAuth 2 for interactive users and Frappe API tokens for non-interactive service clients. It does not need a custom Frappe app and does not use frappe_assistant_core.
Live POC
Service | URL |
MCP endpoint |
|
Service MCP endpoint |
|
Site setup |
|
Health check |
|
The setup page requires the private ADMIN_TOKEN Worker secret. Registered site names and URLs remain private.
Related MCP server: Frappe MCP Server
Usage logs
Authenticated MCP requests on /mcp and /service/mcp emit a structured mcp_request event to Cloudflare Workers Logs. Each event contains site_id, site_name, auth_type (oauth or api_token), a UTC start timestamp, a generated request_id, the MCP method, the registered tool name (or null for other methods), outcome, error_code, and duration_ms.
The message column shows a short summary, such as MCP list_sales_invoices: success or MCP list_suppliers: error (PERMISSION_DENIED). It uses only the recorded method/tool, outcome, and safe error code; the other fields remain available for filtering and grouping.
Events are recorded when the MCP server produces a terminal reply. Tool errors are recorded as error even when the HTTP status is 200 or a service client uses X-MCP-Error-Format: result. Connections closed before a reply are recorded as cancelled. These events describe server execution, not confirmation that the client received the reply. Notifications and requests rejected before MCP dispatch, including authentication failures and malformed transport requests, do not produce usage events. Standard Worker invocation logs still cover HTTP requests.
Site identity comes from the authenticated connection. Usage logs exclude user email addresses, credentials, headers, request arguments, response data, and error text. Unregistered tool and method names are recorded as unknown. Site names are private operational data: keep log access limited to the operators who need it. No tool-usage history is written to D1.
After deployment, open Workers & Pages > nexwave-mcp > Observability, then filter events by event = mcp_request. Add site_name, tool, outcome, and duration_ms as columns. In the account-level Observability > Query Builder, filter to this Worker and event, then count events grouped by site_name and tool, or filter outcome = error to investigate failures. Save these queries for repeated use. The existing Wrangler configuration enables logs with 100% head sampling; storage limits and retention still depend on the Cloudflare plan.
See Workers Logs and Query Builder.
Architecture
flowchart LR
ChatGPT["ChatGPT"]
Claude["Claude"]
Codex["Codex"]
Service["Non-interactive service client"]
MCP["NexWave MCP Server<br/>Cloudflare Worker"]
Site["NexWave / ERPNext Site<br/>Frappe REST API"]
D1[("Cloudflare D1<br/>Site registry, encrypted credentials,<br/>service token hashes, and audit events")]
KV[("Cloudflare KV<br/>OAuth state, grants, and tokens")]
Admin["NexWave administrator"]
ChatGPT -->|"Remote MCP over HTTPS<br/>OAuth 2"| MCP
Claude -->|"Remote MCP over HTTPS<br/>OAuth 2"| MCP
Codex -->|"Remote MCP over HTTPS<br/>OAuth 2"| MCP
Service -->|"Remote MCP over HTTPS<br/>Service token"| MCP
MCP -->|"Frappe REST API<br/>User OAuth token"| Site
MCP -->|"Frappe REST API<br/>Service API token"| Site
MCP --> D1
MCP --> KV
Admin -->|"Registers site credentials"| MCPThe Worker is the protocol and security boundary between each AI client and each NexWave site.
Component | Responsibility |
MCP client | Discovers the server, authenticates, and calls tools |
Cloudflare Worker | Handles MCP, OAuth, tool validation, and Frappe REST requests |
D1 | Stores the private site registry, encrypted OAuth or API-token credentials, service token hashes, and sign-in audit events |
KV | Stores short-lived OAuth state, grants, and MCP tokens |
NexWave site | Authenticates the user and applies normal Frappe permissions |
Authentication and request flow
The MCP client discovers the protected resource and OAuth metadata from the Worker.
The Worker asks the user for their exact NexWave site URL. It does not show a list of registered sites.
The Worker matches the URL origin against its private D1 registry.
The browser moves to the selected NexWave site for Frappe OAuth approval with PKCE S256.
After approval, the Worker returns control to the MCP client and issues an MCP token with the
nexwave:readscope.Each tool call uses the signed-in user's Frappe access token. Frappe applies that user's roles and document permissions.
Non-interactive clients can use the separate /service/mcp route. When an administrator registers a fixed API-token site, the Worker verifies the Frappe credentials and returns a generated service token once. It stores only the service-token hash and encrypted Frappe credentials in D1. A service client sends the generated token in the X-NexWave-Service-Token header. This route exposes the same read-only tool catalogue as /mcp.
Legacy JSON-RPC service tool calls receive finite application/json replies. Standard MCP tool failures retain isError: true by default. Clients that discard these errors can opt in with X-MCP-Error-Format: result. In that mode only, tool failures are delivered as normal MCP results containing status: "error", ok: false, and a safe error object with code, message, and retryable. The client must check this envelope before using any data. Authentication failures and protocol errors keep their normal semantics. The OAuth /mcp route does not use this option.
Each MCP data tool has a shared 50-second upstream request budget, including any lookups and bounded retries of retryable reads. The service response handler allows 60 seconds to deliver the result or a safe error. Configure Retell's MCP timeout_ms as 90000 so it does not stop waiting before the server can return that error. OAuth sign-in and token-exchange timeouts are unchanged.
Connect a client
Use this remote MCP endpoint in a client that supports OAuth-enabled remote MCP servers:
https://mcp.highflyertech.co.nz/mcpFor Codex CLI and the Codex desktop app:
codex mcp add nexwave --url https://mcp.highflyertech.co.nz/mcp
codex mcp login nexwave
codex mcp get nexwaveThe browser will ask for the NexWave site URL and then show that site's normal Frappe sign-in and access approval screens.
Connect a non-interactive service client
Use the service endpoint when a client cannot complete an interactive OAuth flow:
https://mcp.highflyertech.co.nz/service/mcpConfigure this request header:
X-NexWave-Service-Token: <generated-service-token>Add the site at /admin, select Fixed API token, and enter the API key and secret of a dedicated NexWave user. Copy the generated service token when the connection is saved. The same site URL can have one OAuth connection and one fixed-token connection. The gateway token is not a Frappe credential. The Worker uses its hash to select the registered site, decrypts the stored Frappe API token, and applies that service user's normal roles and permissions.
Available tools
All tools are read-only. List results and report output are bounded to keep MCP responses manageable.
Connection and master data
Tool | Purpose |
| Show the connected site and user |
| List accessible companies |
| Search customers |
| Search suppliers |
| Search items |
| List company warehouses |
| List company ledger accounts |
| List company cost centres |
| List company projects |
| List enabled fiscal years |
Transactions
Tool | Purpose |
| List sales orders |
| Search and filter sales invoices |
| Search and filter purchase orders |
| Search and filter purchase invoices |
| Search and filter payment entries |
| Search and filter imported bank transactions |
| Get an allowlisted business document by exact name |
get_document supports Customer, Item, Sales Order, Sales Invoice, Purchase Order, Purchase Invoice, Payment Entry, and Bank Transaction.
Reports
Tool | Standard ERPNext report |
| Stock Balance |
| Stock Ledger |
| Profit and Loss Statement |
| Trial Balance |
| General Ledger |
| Accounts Receivable |
| Accounts Receivable totals, confirmed overdue, and top customer balances |
| Accounts Payable |
| Bank Reconciliation Statement |
Report names and filters are allowlisted. Detailed reports have date-range limits and all report responses have row limits.
Search and voice queries
Directory tools search record IDs and display names. If the full phrase has no exact match, they make one bounded token search and compare normalized names, including common legal suffixes for companies and parties. Fallbacks preserve explicit filters and permissions. Partial or ambiguous names require confirmation. Empty search strings and the existing text-array list responses remain supported; structuredContent adds match status, candidates, and truncation metadata.
Invoice and order tools accept customer_query or supplier_query for spoken names. Exact customer and supplier filters must use confirmed record IDs. Do not combine a spoken query and its exact filter. search is document text, not an unrestricted filter language. SQL wildcard characters in text are escaped.
Use unpaid_only: true for submitted positive invoice balances, including partly paid and overdue invoices. Use overdue_as_of for due dates strictly before a date. Neither can be combined with status. Purchase orders support pending_receipt: true for submitted, open orders with less than 100 percent received. List tools expose bounded, allowlisted sorting. Rank invoice/order base_grand_total with one company, grand_total with an exact document currency, and invoice outstanding_amount with an exact party_account_currency.
Tool | Purpose |
| Resolve a spoken customer/supplier name and return its company-currency balance; use |
| Complete payable totals, credits, overdue balances and top parties |
| Net invoice sales by customer, item, item group or month, calculated before limiting displayed groups |
| Partial stock check against per-warehouse reorder settings, with explicit missing-setting and coverage warnings |
List tools accept offset (default 0). Repeat the same search, filters, limit, and ordering with the returned meta.next_offset. Ordering includes a document-name tie-breaker. A full final page can require one empty request to confirm the end. Records can change between calls; pages are not a snapshot. The first text block remains the legacy record array. A second text block contains the same completeness and scope metadata as structuredContent, including delivery and ranking warnings.
Directory name searches keep their bounded ranked candidates by default. Their offsets page through that candidate set only. Use search_mode: "literal" from offset 0 to page through all literal text matches. Metadata distinguishes ranked_candidates from literal_matches and warns when candidate retrieval reaches its bound.
Use summary: "count" on a list tool to count all permission-visible matching documents, independent of offset and limit. Use literal mode when combining a directory search with a summary. Transaction lists also support summary: "totals": invoice and order grand_total requires one company and document currency; payment base_paid_amount and base_received_amount require one company and use company currency; bank deposit, withdrawal, and unallocated totals require company and bank currency. Aggregates use fixed server-owned expressions through the same permission-aware Frappe list API. Filters, including document state, are retained. These are current document amounts, not recognised revenue, historical balances, or net cash. Use the receivables/payables tools for balances; invoice outstanding amounts can use a different party account currency.
For example, list_sales_invoices with { "company": "Example Company", "currency": "NZD", "docstatus": 1, "summary": "totals" } returns the complete matching document count and grand total without returning invoice pages.
Invoice lists and get_document include party_account_currency beside outstanding_amount. Sorting invoices by outstanding_amount requires an exact party_account_currency filter; a document currency filter alone does not make balances comparable. Sorting grand_total still requires document currency, and sorting base_grand_total requires one company. Ranking metadata names the amount field, currency, and source currency field in both structured and text output. Invoice total summaries remain in document currency.
All 30 tools advertise object output schemas and read-only annotations. Successful results include structuredContent with their existing text content preserved. List contracts cover pages, count/total summaries, and party-confirmation results. Master-data tools advertise summary: "count" only; the six transaction lists also advertise "totals". These annotations describe behavior; Frappe still enforces permissions.
Standard row-based report tools accept offset and return next_offset, row_count, and visible completeness warnings. Each call reruns the report; output pagination does not reduce upstream report size or execution time. Compact summaries retain their existing complete totals and top-result limits.
get_document now returns selected item fields for sales/purchase orders and invoices, invoice allocation references for Payment Entry, and payment links for Bank Transaction. Use detail_limit (default 20, maximum 50) and detail_offset with detail_meta.<table>.next_offset. Each table reports its full row count and page completeness. Missing tables remain unavailable. An empty table can also mean field permissions hid its contents, so empty-table completeness is not claimed. Payment details include both account currencies for reference-amount context. Custom fields and internal fields are excluded; Frappe still enforces parent-document and field permissions.
Date-range reports accept either the existing explicit from_date/to_date pair or period plus as_of_date (the user's current local calendar date). Presets are current_fiscal_year, last_month, and last_90_days. Fiscal years are resolved by NexWave for the specified company. Do not mix presets with explicit dates. Ledger and sales-summary ranges remain bounded to 366 days.
Tool execution shares a 50-second upstream budget for service-token and OAuth clients. Responses are bounded to 8 MiB. Upstream timeouts, invalid JSON, permission failures and oversized results return safe MCP errors; none mean that no records exist. Read-only upstream requests get at most one retry inside the same 50-second budget when the failure is retryable. Authentication routes, stored credentials and null-argument normalization are unchanged. No database migration is needed.
The stock-risk report covers existing item/warehouse stock rows only. It does not implement missing-Bin replenishment, warehouse-group reorder rules, demand forecasts or lead-time forecasts. A zero returned risk count is not evidence that all stock is safe.
The deployment-ready voice prompt and evaluation cases describe the intended client behavior. Deploy the MCP changes, refresh the client's tool catalogue, and then publish the updated voice configuration. Keep all existing tools selected.
The receivable summary adds totals.overdue when overdue_complete is true. It uses positive document balances due strictly before report_date, includes the overdue part of the first ageing bucket, and does not deduct unallocated credits. Missing due dates fall back to posting dates, as in the standard report. If dates or per-customer detail reconciliation are incomplete, the overdue field is omitted rather than reported as zero. Existing totals, ageing buckets and filters are unchanged, and no extra upstream report request is required. The voice prompt also supports sequential, customer-scoped summaries across balances, sales and relevant invoice/order tools, with explicit partial-result handling.
Party and payable summaries also expose overdue_complete and omit overdue amounts when positive documents lack valid dates, while preserving their other balance totals. The prompt requires this confirmation on all three summaries. It can be published safely before the MCP update, but will report overdue as unavailable until the new field is deployed.
Local development
Requirements:
Node.js 22 or newer
A Cloudflare account for deployment
A Frappe v15, ERPNext, or NexWave site
Install dependencies, create local storage, and start the Worker:
npm ci
cp .dev.vars.example .dev.vars
npm run db:local
npm run devOpen http://localhost:8787/admin.
For an OAuth site, create an OAuth Client on the Frappe site with these settings:
Field | Value |
App Name | NexWave MCP |
Scopes |
|
Redirect URIs |
|
Default Redirect URI |
|
Grant Type | Authorization Code |
Response Type | Code |
Skip Authorization | Off |
Copy the generated client ID and secret into the local setup page and select OAuth 2. A local Frappe bench can use http://localhost:8000 when the required site is the bench default.
For a non-interactive service connection, create an API key and secret for a dedicated NexWave user. In the setup page, select Fixed API token. The page shows the generated service token once after it verifies and saves the connection.
Cloudflare deployment
The Worker needs one KV namespace and one D1 database. Replace the account and resource IDs in wrangler.jsonc before deploying a fork to another Cloudflare account.
Set the Worker secrets. Do not store them in source control:
openssl rand -base64 32 | wrangler secret put ADMIN_TOKEN
openssl rand -base64 32 | wrangler secret put CONFIG_ENCRYPTION_KEYApply migrations and deploy:
npm run db:remote
npm run deployFor a Worker at https://nexwave-mcp.example.workers.dev, add this redirect URI to the Frappe OAuth Client:
https://nexwave-mcp.example.workers.dev/oauth/frappe/callbackRegister the site at /admin, then give users the /mcp endpoint.
GitHub Actions validates each push and pull request to main. It does not deploy automatically.
Security model
Site OAuth client secrets and fixed API-token credentials are encrypted with AES-256-GCM before D1 storage.
The OAuth Provider library encrypts upstream tokens and refresh properties in KV.
OAuth approval state expires after ten minutes and is bound to the same browser with an HTTP-only cookie.
Frappe sign-in uses PKCE S256 and the confidential client secret.
The admin API requires a separate bearer token.
The service MCP route accepts a generated gateway token and maps its stored hash to one registered fixed-token site.
A generated service token is returned only once. D1 stores its hash, not the token value.
Fixed Frappe API credentials are encrypted in D1 and are never returned to the MCP client.
Production site URLs must use HTTPS. Plain HTTP is accepted only for local development hosts.
Tools cannot call arbitrary Frappe methods, reports, DocTypes, fields, or URLs.
The public consent page does not enumerate registered site names or URLs.
Frappe remains the source of truth for user permissions.
The POC records successful and failed sign-in events. It does not yet include a full audit viewer, write tools, per-tool scopes, automatic deployment, or managed secret rotation.
Quality checks
Run the same validation used by CI:
npm run check
npm test
npm run test:runtime
npm run test:live # Requires Docker Compose v2
npx wrangler deploy --dry-runtest:live creates a fresh ERPNext v15.109.0 site, MariaDB, and Redis from pinned container images. It bundles the actual Worker entry point, applies D1 migrations, registers generated API tokens, and calls /service/mcp. A test-only network bridge forwards requests to the real local Frappe API. Expected amounts come from synthetic invoices and payments, not a second copy of the tool query.
The suite checks list pages, counts, totals, document and account currencies, item and payment-reference details, receivables/payables, sales, General Ledger, and access for three API users (all companies, one company, and no accounting access). It uses fixed January 2025 dates. The script removes its containers, volumes, and temporary credentials on exit. CI needs no repository secrets. Neither credentials nor site logs are uploaded.
The Live Frappe and Worker checks jobs must both pass for the required Validate check to pass. This first backend covers the standard ERPNext API contract; NexWave app overrides and OAuth browser login need separate coverage.
The protected main branch requires a pull request, resolved review conversations, and a passing Validate check. Contributors need one approval; the current repository administrators can bypass the approval requirement.
See AGENTS.md for contributor and coding-agent rules.
This server cannot be deployed
Maintenance
Related MCP Connectors
Connect MCP clients to 2,000+ AI models without managing provider API keys.
MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.
Native MCP access to CRM contacts, organizations, deals and tasks with user permissions.
Connect any AI agent to 1,000+ apps and 27,000+ actions through one remote MCP server (OAuth).
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables ERPNext management, file operations, read-only database access, and ERPNext API integration through a standardized MCP server.4MIT
- AlicenseAqualityBmaintenanceConnects MCP clients to Frappe/ERPNext sites via REST API, enabling document CRUD, search, and remote method calls.71MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP-compatible clients to connect to any Frappe or Frappe HRMS site, exposing schema discovery, document CRUD and workflow operations, and HR helpers such as leave and attendance management through standardized tools.MIT
- FlicenseNot gradedqualityCmaintenanceEnables MCP-compatible AI clients to securely read data from customers' ERPNext/Frappe sites through a multi-tenant OAuth-protected gateway, with tools for listing, searching, counting, retrieving, and reporting on documents.-