@armor/zuora-mcp 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +16 -0
- package/README.md +246 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +56 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +146 -0
- package/dist/index.js.map +1 -0
- package/dist/token-manager.d.ts +34 -0
- package/dist/token-manager.d.ts.map +1 -0
- package/dist/token-manager.js +103 -0
- package/dist/token-manager.js.map +1 -0
- package/dist/tools.d.ts +734 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1336 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +448 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/zuora-client.d.ts +119 -0
- package/dist/zuora-client.d.ts.map +1 -0
- package/dist/zuora-client.js +405 -0
- package/dist/zuora-client.js.map +1 -0
- package/package.json +59 -0
package/.env.example
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Zuora OAuth Configuration
|
|
2
|
+
# Required: OAuth2 Client ID (create at Zuora Admin > Settings > Administration > OAuth Clients)
|
|
3
|
+
ZUORA_CLIENT_ID=your-oauth-client-id
|
|
4
|
+
|
|
5
|
+
# Required: OAuth2 Client Secret
|
|
6
|
+
ZUORA_CLIENT_SECRET=your-oauth-client-secret
|
|
7
|
+
|
|
8
|
+
# Required: Zuora base URL for your environment
|
|
9
|
+
# Production US: https://rest.zuora.com
|
|
10
|
+
# Sandbox US: https://rest.apisandbox.zuora.com
|
|
11
|
+
# Production EU: https://rest.eu.zuora.com
|
|
12
|
+
# Sandbox EU: https://rest.sandbox.eu.zuora.com
|
|
13
|
+
ZUORA_BASE_URL=https://rest.apisandbox.zuora.com
|
|
14
|
+
|
|
15
|
+
# Optional: Enable debug logging (true/false)
|
|
16
|
+
DEBUG=false
|
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Zuora MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that enables Claude to interact with Zuora's billing platform. Query accounts, invoices, subscriptions, payments, and execute ZOQL queries directly from Claude Code.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js 20+
|
|
10
|
+
- Zuora OAuth credentials (create at Zuora Admin > Settings > Administration > OAuth Clients)
|
|
11
|
+
- npm access to the `@armor` scope (see [npm Setup](#npm-setup))
|
|
12
|
+
|
|
13
|
+
### Install from npm (Recommended)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @armor/zuora-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then add to your Claude Code MCP config (`~/.claude/.mcp.json`):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"zuora": {
|
|
25
|
+
"command": "zuora-mcp",
|
|
26
|
+
"env": {
|
|
27
|
+
"ZUORA_CLIENT_ID": "your-client-id",
|
|
28
|
+
"ZUORA_CLIENT_SECRET": "your-client-secret",
|
|
29
|
+
"ZUORA_BASE_URL": "https://rest.apisandbox.zuora.com"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Restart Claude Code and the Zuora tools will be available.
|
|
37
|
+
|
|
38
|
+
### npm Setup
|
|
39
|
+
|
|
40
|
+
One-time setup to access the `@armor` npm scope:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Get your npm access token from the team lead or npm admin
|
|
44
|
+
echo "//registry.npmjs.org/:_authToken=<YOUR_NPM_TOKEN>" >> ~/.npmrc
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Install from Source (Alternative)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone git@github.com:armor/zuora-mcp.git
|
|
51
|
+
cd zuora-mcp
|
|
52
|
+
npm install
|
|
53
|
+
npm run build
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When installing from source, point to the built file in your MCP config:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"zuora": {
|
|
62
|
+
"command": "node",
|
|
63
|
+
"args": ["/path/to/zuora-mcp/dist/index.js"],
|
|
64
|
+
"env": {
|
|
65
|
+
"ZUORA_CLIENT_ID": "your-client-id",
|
|
66
|
+
"ZUORA_CLIENT_SECRET": "your-client-secret",
|
|
67
|
+
"ZUORA_BASE_URL": "https://rest.apisandbox.zuora.com"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Configuration
|
|
75
|
+
|
|
76
|
+
Required environment variables:
|
|
77
|
+
|
|
78
|
+
| Variable | Description |
|
|
79
|
+
|----------|-------------|
|
|
80
|
+
| `ZUORA_CLIENT_ID` | OAuth2 client ID |
|
|
81
|
+
| `ZUORA_CLIENT_SECRET` | OAuth2 client secret |
|
|
82
|
+
| `ZUORA_BASE_URL` | One of the Zuora base URLs (see below) |
|
|
83
|
+
|
|
84
|
+
Optional variables:
|
|
85
|
+
|
|
86
|
+
| Variable | Description |
|
|
87
|
+
|----------|-------------|
|
|
88
|
+
| `DEBUG` | Set to `true` for verbose logging |
|
|
89
|
+
|
|
90
|
+
#### Zuora Base URLs
|
|
91
|
+
|
|
92
|
+
| Environment | URL |
|
|
93
|
+
|-------------|-----|
|
|
94
|
+
| US Production | `https://rest.zuora.com` |
|
|
95
|
+
| US Sandbox | `https://rest.apisandbox.zuora.com` |
|
|
96
|
+
| EU Production | `https://rest.eu.zuora.com` |
|
|
97
|
+
| EU Sandbox | `https://rest.sandbox.eu.zuora.com` |
|
|
98
|
+
|
|
99
|
+
### Getting Zuora OAuth Credentials
|
|
100
|
+
|
|
101
|
+
1. Log into Zuora (production or sandbox)
|
|
102
|
+
2. Navigate to **Settings > Administration > Manage Users**
|
|
103
|
+
3. Select your user or create a service account
|
|
104
|
+
4. Go to **OAuth Clients** tab and create a new client
|
|
105
|
+
5. Copy the **Client ID** and **Client Secret** (secret is shown only once)
|
|
106
|
+
|
|
107
|
+
## Available Tools
|
|
108
|
+
|
|
109
|
+
### Account Tools
|
|
110
|
+
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
|------|-------------|
|
|
113
|
+
| `get_account` | Get account details by key (ID or account number) |
|
|
114
|
+
| `get_account_summary` | Get comprehensive account summary with balances, subscriptions, invoices, and payments |
|
|
115
|
+
|
|
116
|
+
### Invoice Tools
|
|
117
|
+
|
|
118
|
+
| Tool | Description |
|
|
119
|
+
|------|-------------|
|
|
120
|
+
| `get_invoice` | Get invoice details including line items |
|
|
121
|
+
| `list_invoices` | List invoices for an account with pagination |
|
|
122
|
+
|
|
123
|
+
### Subscription Tools
|
|
124
|
+
|
|
125
|
+
| Tool | Description |
|
|
126
|
+
|------|-------------|
|
|
127
|
+
| `get_subscription` | Get subscription details with rate plans and charges |
|
|
128
|
+
| `list_subscriptions` | List all subscriptions for an account |
|
|
129
|
+
|
|
130
|
+
### Payment Tools
|
|
131
|
+
|
|
132
|
+
| Tool | Description |
|
|
133
|
+
|------|-------------|
|
|
134
|
+
| `get_payment` | Get payment details |
|
|
135
|
+
| `list_payments` | List payments with optional account filter and pagination |
|
|
136
|
+
|
|
137
|
+
### ZOQL Query Tools
|
|
138
|
+
|
|
139
|
+
| Tool | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `execute_zoql_query` | Execute a ZOQL query for ad-hoc data retrieval |
|
|
142
|
+
| `continue_zoql_query` | Continue paginated ZOQL results using queryLocator |
|
|
143
|
+
|
|
144
|
+
### Product Catalog
|
|
145
|
+
|
|
146
|
+
| Tool | Description |
|
|
147
|
+
|------|-------------|
|
|
148
|
+
| `list_products` | List products with rate plans and pricing |
|
|
149
|
+
|
|
150
|
+
## Upgrading
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm update -g @armor/zuora-mcp
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm run dev # Run with tsx (no build required)
|
|
160
|
+
npm run build # Build TypeScript to dist/
|
|
161
|
+
npm run lint # Run ESLint
|
|
162
|
+
npm run typecheck # Type check without building
|
|
163
|
+
npm test # Build and run tests
|
|
164
|
+
npm run clean # Remove dist/
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Debug Mode
|
|
168
|
+
|
|
169
|
+
Set `DEBUG=true` to enable verbose logging to stderr:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
DEBUG=true npm start
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Debug output goes to stderr to avoid corrupting the stdio JSON-RPC transport.
|
|
176
|
+
|
|
177
|
+
### Release Process
|
|
178
|
+
|
|
179
|
+
This project uses semantic-release with branch-based publishing:
|
|
180
|
+
|
|
181
|
+
| Branch | npm Tag | Description |
|
|
182
|
+
|--------|---------|-------------|
|
|
183
|
+
| `dev` | `alpha` | Development pre-releases |
|
|
184
|
+
| `stage` | `beta` | Staging pre-releases |
|
|
185
|
+
| `main` | `latest` | Production releases |
|
|
186
|
+
|
|
187
|
+
Merging to any of these branches triggers the CD pipeline which:
|
|
188
|
+
1. Runs lint, typecheck, build, and tests
|
|
189
|
+
2. Determines the next version from commit messages
|
|
190
|
+
3. Creates a GitHub release with notes
|
|
191
|
+
4. Publishes to npm with the appropriate tag
|
|
192
|
+
|
|
193
|
+
#### Commit Convention
|
|
194
|
+
|
|
195
|
+
Follow conventional commits for automatic version bumps:
|
|
196
|
+
|
|
197
|
+
| Prefix | Version Bump |
|
|
198
|
+
|--------|--------------|
|
|
199
|
+
| `feat:` | Minor (1.x.0) |
|
|
200
|
+
| `fix:` | Patch (1.0.x) |
|
|
201
|
+
| `breaking:` | Major (x.0.0) |
|
|
202
|
+
|
|
203
|
+
## Architecture
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
src/
|
|
207
|
+
├── index.ts # MCP server entry point
|
|
208
|
+
├── config.ts # Zod-validated environment configuration
|
|
209
|
+
├── token-manager.ts # OAuth2 token lifecycle (refresh, coalescing)
|
|
210
|
+
├── zuora-client.ts # Zuora REST API client with resilience
|
|
211
|
+
├── tools.ts # Tool definitions, Zod schemas, handlers
|
|
212
|
+
└── types.ts # TypeScript interfaces
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Authentication
|
|
216
|
+
|
|
217
|
+
Uses OAuth 2.0 Client Credentials flow:
|
|
218
|
+
- Tokens are stored in memory only (not persisted)
|
|
219
|
+
- Proactive refresh 60 seconds before expiry
|
|
220
|
+
- Promise coalescing prevents duplicate refresh requests under concurrency
|
|
221
|
+
- Automatic 401 retry with token refresh
|
|
222
|
+
|
|
223
|
+
### Resilience
|
|
224
|
+
|
|
225
|
+
- Exponential backoff with jitter on 429/502/503/504
|
|
226
|
+
- Idempotent methods (GET, PUT, DELETE) retry on transport failures
|
|
227
|
+
- POST requests are NOT retried on HTTP errors (financial safety)
|
|
228
|
+
- 20-second request timeout with AbortController
|
|
229
|
+
|
|
230
|
+
### Security
|
|
231
|
+
|
|
232
|
+
- All inputs validated with Zod schemas
|
|
233
|
+
- Sensitive fields (card numbers, bank accounts) redacted from debug logs
|
|
234
|
+
- Bearer tokens scrubbed from error messages
|
|
235
|
+
- OAuth credentials never logged
|
|
236
|
+
- Base URL validated against known Zuora endpoints at startup
|
|
237
|
+
|
|
238
|
+
## Version History
|
|
239
|
+
|
|
240
|
+
### v1.0.0
|
|
241
|
+
|
|
242
|
+
- Initial release with 11 read-only tools
|
|
243
|
+
- OAuth 2.0 authentication with token lifecycle management
|
|
244
|
+
- Account, invoice, subscription, payment, and ZOQL query support
|
|
245
|
+
- Product catalog access
|
|
246
|
+
- Resilient HTTP client with retry and backoff
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Zuora MCP Server
|
|
3
|
+
* Security-first: No hardcoded values, all from environment
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
declare const configSchema: z.ZodObject<{
|
|
7
|
+
zuoraClientId: z.ZodString;
|
|
8
|
+
zuoraClientSecret: z.ZodString;
|
|
9
|
+
zuoraBaseUrl: z.ZodEffects<z.ZodString, string, string>;
|
|
10
|
+
debug: z.ZodDefault<z.ZodBoolean>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
zuoraClientId: string;
|
|
13
|
+
zuoraClientSecret: string;
|
|
14
|
+
zuoraBaseUrl: string;
|
|
15
|
+
debug: boolean;
|
|
16
|
+
}, {
|
|
17
|
+
zuoraClientId: string;
|
|
18
|
+
zuoraClientSecret: string;
|
|
19
|
+
zuoraBaseUrl: string;
|
|
20
|
+
debug?: boolean | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
export type Config = z.infer<typeof configSchema>;
|
|
23
|
+
export declare function loadConfig(): Config;
|
|
24
|
+
export declare function validateConfig(): void;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgBxB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;EAoBhB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD,wBAAgB,UAAU,IAAI,MAAM,CAkBnC;AAED,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Zuora MCP Server
|
|
3
|
+
* Security-first: No hardcoded values, all from environment
|
|
4
|
+
*/
|
|
5
|
+
import { config as dotenvConfig } from "dotenv";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
// Load .env files with quiet mode to prevent stdout corruption of JSON-RPC.
|
|
8
|
+
// Precedence: process env > .env.local > .env
|
|
9
|
+
// Base .env does NOT use override so process-level env vars take precedence.
|
|
10
|
+
// .env.local uses override so it can override both .env and process env for local dev.
|
|
11
|
+
dotenvConfig({ path: ".env", quiet: true });
|
|
12
|
+
dotenvConfig({ path: ".env.local", override: true, quiet: true });
|
|
13
|
+
const VALID_ZUORA_BASE_URLS = new Set([
|
|
14
|
+
"https://rest.zuora.com",
|
|
15
|
+
"https://rest.apisandbox.zuora.com",
|
|
16
|
+
"https://rest.eu.zuora.com",
|
|
17
|
+
"https://rest.sandbox.eu.zuora.com",
|
|
18
|
+
]);
|
|
19
|
+
const configSchema = z.object({
|
|
20
|
+
zuoraClientId: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1, "ZUORA_CLIENT_ID is required")
|
|
23
|
+
.describe("OAuth2 client ID from Zuora Admin"),
|
|
24
|
+
zuoraClientSecret: z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1, "ZUORA_CLIENT_SECRET is required")
|
|
27
|
+
.describe("OAuth2 client secret"),
|
|
28
|
+
zuoraBaseUrl: z
|
|
29
|
+
.string()
|
|
30
|
+
.url("ZUORA_BASE_URL must be a valid URL")
|
|
31
|
+
.refine((url) => VALID_ZUORA_BASE_URLS.has(url.replace(/\/$/, "")), {
|
|
32
|
+
message: `ZUORA_BASE_URL must be one of: ${[...VALID_ZUORA_BASE_URLS].join(", ")}`,
|
|
33
|
+
})
|
|
34
|
+
.describe("Zuora REST API base URL"),
|
|
35
|
+
debug: z.boolean().default(false).describe("Enable debug logging"),
|
|
36
|
+
});
|
|
37
|
+
export function loadConfig() {
|
|
38
|
+
const rawConfig = {
|
|
39
|
+
zuoraClientId: process.env.ZUORA_CLIENT_ID,
|
|
40
|
+
zuoraClientSecret: process.env.ZUORA_CLIENT_SECRET,
|
|
41
|
+
zuoraBaseUrl: process.env.ZUORA_BASE_URL?.replace(/\/$/, ""),
|
|
42
|
+
debug: process.env.DEBUG === "true",
|
|
43
|
+
};
|
|
44
|
+
const result = configSchema.safeParse(rawConfig);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
const errors = result.error.errors
|
|
47
|
+
.map((e) => ` - ${e.path.join(".")}: ${e.message}`)
|
|
48
|
+
.join("\n");
|
|
49
|
+
throw new Error(`Configuration validation failed:\n${errors}`);
|
|
50
|
+
}
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
53
|
+
export function validateConfig() {
|
|
54
|
+
loadConfig();
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,4EAA4E;AAC5E,8CAA8C;AAC9C,6EAA6E;AAC7E,uFAAuF;AACvF,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5C,YAAY,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAElE,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,wBAAwB;IACxB,mCAAmC;IACnC,2BAA2B;IAC3B,mCAAmC;CACpC,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,EAAE,6BAA6B,CAAC;SACrC,QAAQ,CAAC,mCAAmC,CAAC;IAChD,iBAAiB,EAAE,CAAC;SACjB,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,EAAE,iCAAiC,CAAC;SACzC,QAAQ,CAAC,sBAAsB,CAAC;IACnC,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,GAAG,CAAC,oCAAoC,CAAC;SACzC,MAAM,CACL,CAAC,GAAG,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAC1D;QACE,OAAO,EAAE,kCAAkC,CAAC,GAAG,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;KACnF,CACF;SACA,QAAQ,CAAC,yBAAyB,CAAC;IACtC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC;CACnE,CAAC,CAAC;AAIH,MAAM,UAAU,UAAU;IACxB,MAAM,SAAS,GAAG;QAChB,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe;QAC1C,iBAAiB,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;QAClD,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC5D,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,MAAM;KACpC,CAAC;IAEF,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAEjD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAC/B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;aACnD,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,qCAAqC,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,UAAU,EAAE,CAAC;AACf,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Zuora MCP Server
|
|
4
|
+
*
|
|
5
|
+
* A Model Context Protocol server for Zuora billing operations.
|
|
6
|
+
* Provides account, invoice, subscription, payment, and ZOQL query tools.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - OAuth 2.0 authentication with automatic token lifecycle
|
|
10
|
+
* - Account and billing entity read operations
|
|
11
|
+
* - ZOQL query support with pagination
|
|
12
|
+
* - Product catalog access
|
|
13
|
+
* - Resilient HTTP client with retry and backoff
|
|
14
|
+
*
|
|
15
|
+
* Security:
|
|
16
|
+
* - No hardcoded credentials
|
|
17
|
+
* - Input validation with Zod schemas
|
|
18
|
+
* - Sensitive data redaction in logs
|
|
19
|
+
* - OAuth token promise coalescing for concurrency safety
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;GAkBG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Zuora MCP Server
|
|
4
|
+
*
|
|
5
|
+
* A Model Context Protocol server for Zuora billing operations.
|
|
6
|
+
* Provides account, invoice, subscription, payment, and ZOQL query tools.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - OAuth 2.0 authentication with automatic token lifecycle
|
|
10
|
+
* - Account and billing entity read operations
|
|
11
|
+
* - ZOQL query support with pagination
|
|
12
|
+
* - Product catalog access
|
|
13
|
+
* - Resilient HTTP client with retry and backoff
|
|
14
|
+
*
|
|
15
|
+
* Security:
|
|
16
|
+
* - No hardcoded credentials
|
|
17
|
+
* - Input validation with Zod schemas
|
|
18
|
+
* - Sensitive data redaction in logs
|
|
19
|
+
* - OAuth token promise coalescing for concurrency safety
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24
|
+
import { CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
+
import { loadConfig } from "./config.js";
|
|
26
|
+
import { ZuoraClient } from "./zuora-client.js";
|
|
27
|
+
import { toolRegistrations, ToolHandlers } from "./tools.js";
|
|
28
|
+
const SERVER_NAME = "zuora-mcp";
|
|
29
|
+
function getServerVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
32
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
33
|
+
if (typeof packageJson.version === "string" &&
|
|
34
|
+
packageJson.version.length) {
|
|
35
|
+
return packageJson.version;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Fall through to safe default.
|
|
40
|
+
}
|
|
41
|
+
return "0.0.0";
|
|
42
|
+
}
|
|
43
|
+
function toCallToolResult(result) {
|
|
44
|
+
const structuredContent = {
|
|
45
|
+
success: result.success,
|
|
46
|
+
message: result.message,
|
|
47
|
+
};
|
|
48
|
+
if (result.data !== undefined) {
|
|
49
|
+
structuredContent.data = result.data;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
structuredContent,
|
|
59
|
+
isError: !result.success,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function main() {
|
|
63
|
+
// Validate configuration before starting
|
|
64
|
+
let config;
|
|
65
|
+
try {
|
|
66
|
+
config = loadConfig();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("Configuration error:", error instanceof Error ? error.message : error);
|
|
70
|
+
console.error("\nRequired environment variables:");
|
|
71
|
+
console.error(" ZUORA_CLIENT_ID - OAuth2 client ID from Zuora Admin");
|
|
72
|
+
console.error(" ZUORA_CLIENT_SECRET - OAuth2 client secret");
|
|
73
|
+
console.error(" ZUORA_BASE_URL - Zuora API base URL (e.g., https://rest.test.zuora.com)");
|
|
74
|
+
console.error("\nOptional:");
|
|
75
|
+
console.error(" DEBUG=true - Enable debug logging");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const client = new ZuoraClient(config);
|
|
79
|
+
const handlers = new ToolHandlers(client);
|
|
80
|
+
// Create MCP server
|
|
81
|
+
const server = new McpServer({
|
|
82
|
+
name: SERVER_NAME,
|
|
83
|
+
version: getServerVersion(),
|
|
84
|
+
});
|
|
85
|
+
// Register tools for MCP capability advertisement (tool listing).
|
|
86
|
+
// Dispatch is handled by the setRequestHandler override below.
|
|
87
|
+
for (const registration of toolRegistrations) {
|
|
88
|
+
server.registerTool(registration.name, {
|
|
89
|
+
description: registration.description,
|
|
90
|
+
inputSchema: registration.inputSchema,
|
|
91
|
+
}, async (args) => {
|
|
92
|
+
// Stub: actual dispatch is handled by setRequestHandler below.
|
|
93
|
+
const result = await registration.invoke(handlers, args);
|
|
94
|
+
return toCallToolResult(result);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Map-based dispatch with protocol-level error handling.
|
|
98
|
+
// This overrides the SDK's default CallToolRequest handler to provide
|
|
99
|
+
// proper McpError responses for unknown tools and invalid parameters.
|
|
100
|
+
const registrationByName = new Map(toolRegistrations.map((registration) => [
|
|
101
|
+
registration.name,
|
|
102
|
+
registration,
|
|
103
|
+
]));
|
|
104
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
105
|
+
const { name, arguments: args } = request.params;
|
|
106
|
+
const registration = registrationByName.get(name);
|
|
107
|
+
if (!registration) {
|
|
108
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
109
|
+
}
|
|
110
|
+
if (config.debug) {
|
|
111
|
+
console.error(`[${SERVER_NAME}] Tool called: ${registration.name}`, args);
|
|
112
|
+
}
|
|
113
|
+
let validatedArgs;
|
|
114
|
+
try {
|
|
115
|
+
validatedArgs = registration.inputSchema.parse(args);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${registration.name}: ${message}`);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const result = await registration.invoke(handlers, validatedArgs);
|
|
123
|
+
return toCallToolResult(result);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
return toCallToolResult({
|
|
128
|
+
success: false,
|
|
129
|
+
message: `Unhandled tool error: ${message}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// Connect via stdio transport
|
|
134
|
+
const transport = new StdioServerTransport();
|
|
135
|
+
await server.connect(transport);
|
|
136
|
+
if (config.debug) {
|
|
137
|
+
console.error(`[${SERVER_NAME}] Server started successfully`);
|
|
138
|
+
console.error(`[${SERVER_NAME}] Connected to ${config.zuoraBaseUrl}`);
|
|
139
|
+
console.error(`[${SERVER_NAME}] Tools registered: ${toolRegistrations.length}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
main().catch((error) => {
|
|
143
|
+
console.error("Fatal error:", error);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Token Manager for Zuora API
|
|
3
|
+
*
|
|
4
|
+
* Handles token lifecycle with:
|
|
5
|
+
* - Proactive refresh before expiry (60s buffer)
|
|
6
|
+
* - Promise coalescing for concurrent refresh requests
|
|
7
|
+
* - Clean error propagation on auth failures
|
|
8
|
+
*/
|
|
9
|
+
import type { Config } from "./config.js";
|
|
10
|
+
export declare class TokenManager {
|
|
11
|
+
private static readonly EXPIRY_BUFFER_MS;
|
|
12
|
+
private static readonly TOKEN_REQUEST_TIMEOUT_MS;
|
|
13
|
+
private readonly clientId;
|
|
14
|
+
private readonly clientSecret;
|
|
15
|
+
private readonly tokenUrl;
|
|
16
|
+
private readonly debug;
|
|
17
|
+
private tokenData;
|
|
18
|
+
private refreshPromise;
|
|
19
|
+
constructor(config: Config);
|
|
20
|
+
private log;
|
|
21
|
+
/**
|
|
22
|
+
* Get a valid access token. Refreshes proactively before expiry.
|
|
23
|
+
* Concurrent callers coalesce onto a single refresh request.
|
|
24
|
+
*/
|
|
25
|
+
getAccessToken(): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Force a token refresh. Called when a 401 is received mid-session,
|
|
28
|
+
* indicating the token expired between the validity check and the API call.
|
|
29
|
+
*/
|
|
30
|
+
forceRefresh(): Promise<string>;
|
|
31
|
+
private isTokenValid;
|
|
32
|
+
private fetchNewToken;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=token-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../src/token-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQ1C,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAE1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAEhC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,cAAc,CAAgC;gBAE1C,MAAM,EAAE,MAAM;IAO1B,OAAO,CAAC,GAAG;IAMX;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAkBvC;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAKrC,OAAO,CAAC,YAAY;YAON,aAAa;CAsD5B"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Token Manager for Zuora API
|
|
3
|
+
*
|
|
4
|
+
* Handles token lifecycle with:
|
|
5
|
+
* - Proactive refresh before expiry (60s buffer)
|
|
6
|
+
* - Promise coalescing for concurrent refresh requests
|
|
7
|
+
* - Clean error propagation on auth failures
|
|
8
|
+
*/
|
|
9
|
+
export class TokenManager {
|
|
10
|
+
static EXPIRY_BUFFER_MS = 60_000;
|
|
11
|
+
static TOKEN_REQUEST_TIMEOUT_MS = 10_000;
|
|
12
|
+
clientId;
|
|
13
|
+
clientSecret;
|
|
14
|
+
tokenUrl;
|
|
15
|
+
debug;
|
|
16
|
+
tokenData = null;
|
|
17
|
+
refreshPromise = null;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.clientId = config.zuoraClientId;
|
|
20
|
+
this.clientSecret = config.zuoraClientSecret;
|
|
21
|
+
this.tokenUrl = `${config.zuoraBaseUrl}/oauth/token`;
|
|
22
|
+
this.debug = config.debug;
|
|
23
|
+
}
|
|
24
|
+
log(message) {
|
|
25
|
+
if (this.debug) {
|
|
26
|
+
console.error(`[TokenManager] ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get a valid access token. Refreshes proactively before expiry.
|
|
31
|
+
* Concurrent callers coalesce onto a single refresh request.
|
|
32
|
+
*/
|
|
33
|
+
async getAccessToken() {
|
|
34
|
+
if (this.isTokenValid()) {
|
|
35
|
+
return this.tokenData.accessToken;
|
|
36
|
+
}
|
|
37
|
+
// Coalesce concurrent refresh calls into one network request
|
|
38
|
+
if (!this.refreshPromise) {
|
|
39
|
+
this.log("Token expired or missing, fetching new token");
|
|
40
|
+
this.refreshPromise = this.fetchNewToken().finally(() => {
|
|
41
|
+
this.refreshPromise = null;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.log("Awaiting in-flight token refresh");
|
|
46
|
+
}
|
|
47
|
+
return this.refreshPromise;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Force a token refresh. Called when a 401 is received mid-session,
|
|
51
|
+
* indicating the token expired between the validity check and the API call.
|
|
52
|
+
*/
|
|
53
|
+
async forceRefresh() {
|
|
54
|
+
this.tokenData = null;
|
|
55
|
+
return this.getAccessToken();
|
|
56
|
+
}
|
|
57
|
+
isTokenValid() {
|
|
58
|
+
return (this.tokenData !== null &&
|
|
59
|
+
Date.now() < this.tokenData.expiresAt - TokenManager.EXPIRY_BUFFER_MS);
|
|
60
|
+
}
|
|
61
|
+
async fetchNewToken() {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeoutId = setTimeout(() => controller.abort(), TokenManager.TOKEN_REQUEST_TIMEOUT_MS);
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(this.tokenUrl, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
69
|
+
},
|
|
70
|
+
body: new URLSearchParams({
|
|
71
|
+
client_id: this.clientId,
|
|
72
|
+
client_secret: this.clientSecret,
|
|
73
|
+
grant_type: "client_credentials",
|
|
74
|
+
}).toString(),
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorText = await response.text().catch(() => "unknown");
|
|
79
|
+
throw new Error(`OAuth token request failed (${response.status}): ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
const data = (await response.json());
|
|
82
|
+
this.tokenData = {
|
|
83
|
+
accessToken: data.access_token,
|
|
84
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
85
|
+
};
|
|
86
|
+
this.log(`Token acquired, expires in ${data.expires_in}s (scope: ${data.scope})`);
|
|
87
|
+
return this.tokenData.accessToken;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Scrub credentials from any error messages using global regex
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
93
|
+
const sanitized = message
|
|
94
|
+
.replace(new RegExp(escapeRegex(this.clientId), "g"), "[CLIENT_ID]")
|
|
95
|
+
.replace(new RegExp(escapeRegex(this.clientSecret), "g"), "[CLIENT_SECRET]");
|
|
96
|
+
throw new Error(`Token acquisition failed: ${sanitized}`);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timeoutId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=token-manager.js.map
|