@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 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
@@ -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"}
@@ -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