Skip to content

feat: harden API client, align with mcp-ecosystem standards#15

Merged
jack-arturo merged 4 commits into
mainfrom
feat/pre-1-0-0-tests-hardening
Feb 17, 2026
Merged

feat: harden API client, align with mcp-ecosystem standards#15
jack-arturo merged 4 commits into
mainfrom
feat/pre-1-0-0-tests-hardening

Conversation

@jack-arturo

Copy link
Copy Markdown
Member

Summary

Pre-1.0.0 hardening pass to align with mcp-ecosystem standards and fix real-world API edge cases.

API Client Hardening

  • Customer ID fallbackgetCustomerById now tries customer, id, user_id, and user params sequentially (EDD API is inconsistent about which ID field it respects)
  • Stats date-range parsing — Robust value coercion (handles strings, numbers, nested objects) with date range filtering to drop out-of-range entries
  • Discount code normalization — Consistent discountCodes array across list/get sale responses (handles string, array, and missing values)

Standards Alignment

  • eslint-config-prettier integration
  • tsconfig strict flags (noUnusedLocals, noUnusedParameters, noImplicitReturns)
  • prepublishOnly runs tests before publish
  • ESLint errors fixed in stdio.ts console patching
  • manifest.json screenshots point to actual asset
  • npm audit fix — 0 vulnerabilities

Release Infrastructure

  • Release-please manifest mode with config files (bootstrapped at 1.0.0)
  • OIDC Trusted Publishing (no NPM_TOKEN secret needed, Node 24)
  • Auto-build .mcpb Desktop Extension and attach to GitHub Releases
  • Slack announce job on release

Tests

  • 3 new unit tests: customer ID fallback, stats date coercion, product stats parsing
  • 26 total, all passing

Test plan

  • npm run build — clean
  • npm run lint — clean
  • npm test — 26/26 passing
  • npm audit — 0 vulnerabilities

Post-merge

  1. git tag v1.0.0 main && git push origin v1.0.0
  2. npm publish --access public (first-time)
  3. gh release create v1.0.0 --title "v1.0.0" --notes "Initial release"
  4. Set up Trusted Publishing on npmjs.com + npm GitHub environment
  5. ~/mcp-registry/bin/mcp-publisher publish server.json

Made with Cursor

jack-arturo and others added 2 commits January 11, 2026 05:36
Introduces a new tool for validating EDD API connection, enforces MCP stdio protocol safety by redirecting console output to stderr, and adds detailed HTTP error diagnostics with actionable hints. Updates manifest and server metadata, normalizes EDD API URLs, and expands tests for error handling and stdio cleanliness. Also adds brand assets and screenshots.
- Customer ID fallback logic (tries customer, id, user_id, user params)
- Robust stats date-range parsing with value coercion and range filtering
- Discount code normalization across list/get sale responses
- Add edd_validate_connection tool to README
- 3 new unit tests (26 total, all passing)

Standards alignment:
- Add eslint-config-prettier integration
- Add tsconfig strict flags (noUnusedLocals, noUnusedParameters, noImplicitReturns)
- Fix prepublishOnly to run tests before publish
- Fix ESLint errors in stdio.ts console patching
- Fix manifest.json screenshots to reference actual asset
- npm audit fix (0 vulnerabilities)

Release infrastructure:
- Release-please manifest mode with config files
- OIDC Trusted Publishing (no NPM_TOKEN needed, Node 24)
- Auto-build .mcpb Desktop Extension on release
- Slack announce job

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a connection validation tool to verify Store API URL and credentials
    • Enhanced sales data with discount codes field for improved transparency
    • Improved customer data fields with clearer ID mapping and user ID information
  • Bug Fixes

    • Implemented more informative error messages for API connection issues
    • Increased resilience to API response format variations
  • Documentation

    • Expanded manifest metadata with enhanced descriptions, icons, and support information
  • Chores

    • Updated release workflow and publication configuration

Walkthrough

This PR enhances error handling with a structured HTTP error type, adds API URL validation/normalization, implements console output redirection for MCP stdio compatibility, introduces a new connection validation tool, and enriches existing tool outputs. The release workflow is updated to use manifest-based versioning with separate build and publish jobs. ESLint and TypeScript configurations are tightened.

Changes

Cohort / File(s) Summary
Release Infrastructure
.github/workflows/release-please.yml, .release-please-manifest.json, release-please-config.json
Introduces manifest-based release versioning with separate npm-publish, build-extension, and announce jobs. Adds branch protection formatting, OIDC-based publishing without NPM_TOKEN, and .mcpb artifact uploads to releases.
Linting & TypeScript Configuration
eslint.config.mjs, tsconfig.json, package.json
Integrates eslint-config-prettier, adds no-console rule (except error/warn), and enables stricter TypeScript checks (noUnusedLocals, noUnusedParameters, noImplicitReturns). Updates prepublishOnly script to run build and test.
Documentation & Metadata
README.md, manifest.json, server.json
Adds edd_validate_connection tool documentation; enriches manifest.json with long_description, icon, screenshots, author email, homepage, documentation, support, privacy_policies, and expanded keywords; updates EDD API URL endpoint requirements.
HTTP Error Handling
src/edd-client.ts
Introduces EDDHttpError class with structured fields (url, status, statusText, contentType, serverHeader, bodySnippet) and helper utilities to generate contextual error messages (HTML/WAF/Cloudflare detection, auth hints). Replaces generic error throws in HTTP failure paths.
API URL Validation
src/env.ts
Adds normalizeEddApiUrl() function to validate and normalize API URLs, ensuring http/https and /edd-api/ base path. Integrates into validateEnv() with error handling and user guidance.
Console & MCP Protocol
src/stdio.ts
New module patches console.log/info/debug to redirect to stderr, preventing interference with MCP stdio transport via idempotent patchConsoleForMcpStdio().
Tool Enhancements
src/index.ts
Adds edd_validate_connection tool for API connectivity checks. Enriches edd_list_sales and edd_get_sale with discountCodes field. Updates edd_list_customers to map id from customer_id and add userId field for compatibility.
Test Coverage
tests/unit/edd-client.test.ts, tests/unit/env.test.ts, tests/unit/stdio-clean.test.ts
Adds tests for customer ID fallback lookup, date-range stats coercion, product stats array/object handling, HTTP error scenarios with helpful hints, URL normalization edge cases, and stdout cleanliness on startup.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant MCP as MCP Server
    participant Validator as edd_validate_connection
    participant Client as EDD Client
    participant API as EDD API

    User->>MCP: Call edd_validate_connection
    MCP->>Validator: Execute validation tool
    Validator->>Client: listProducts()
    Client->>API: GET /edd-api/products/
    API-->>Client: Response (or error)
    Client-->>Validator: Products list
    Validator->>Client: listSales()
    Client->>API: GET /edd-api/sales/
    API-->>Client: Response (or error)
    Client-->>Validator: Sales list
    Validator->>Validator: Compile checks result
    Validator-->>MCP: {ok: bool, checks: {...}}
    MCP-->>User: Validation status & diagnostics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main objective: hardening the API client and aligning with ecosystem standards, which is directly reflected across multiple files.
Description check ✅ Passed The description is well-structured and directly related to the changeset, covering API client hardening, standards alignment, release infrastructure, and testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pre-1-0-0-tests-hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/env.ts (1)

42-79: Consider using Zod for URL validation.

The manual URL validation works correctly, but coding guidelines specify using Zod schemas for input validation. This would provide consistent error formatting and integrate with the existing Zod infrastructure.

As per coding guidelines: "Use Zod schemas for input validation of tool parameters" - while this isn't a tool parameter, using Zod for env validation would maintain consistency.

♻️ Optional Zod-based alternative
import { z } from 'zod';

const EddApiUrlSchema = z.string().url().transform((rawUrl) => {
  const parsedUrl = new URL(rawUrl);
  if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
    throw new Error('EDD_API_URL must start with https:// or http://');
  }
  parsedUrl.search = '';
  parsedUrl.hash = '';
  // ... rest of normalization logic
  return parsedUrl.toString();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/env.ts` around lines 42 - 79, Replace the manual URL validation in
normalizeEddApiUrl with a Zod schema: create an EddApiUrlSchema using
z.string().url().transform that parses the URL, enforces protocol is 'https:' or
'http:' (throwing the same protocol error), strips search and hash, and performs
the same pathname normalization logic (edd-api prefix handling and trailing
slashes) before returning the normalized string; then refactor
normalizeEddApiUrl to either use EddApiUrlSchema.parse(rawUrl) or delegate to
the schema's transform so validation/error formatting is consistent with the
project's Zod infrastructure.
src/stdio.ts (1)

5-9: Suspicious type cast in writeStderr.

The cast args as [] is incorrect—it casts the array to an empty tuple type, which doesn't match the format function's variadic signature. While this works at runtime due to JavaScript's spread behavior, it's misleading and could hide type errors.

♻️ Suggested fix
 function writeStderr(prefix: string | null, args: unknown[]): void {
-  const message = format(...(args as []));
+  const message = format(...args);
   const line = prefix ? `${prefix}${message}\n` : `${message}\n`;
   process.stderr.write(line);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stdio.ts` around lines 5 - 9, The cast `args as []` in writeStderr is
incorrect and misleading; instead remove that empty-tuple cast and spread args
with a correct type assertion compatible with format (e.g., use format(...(args
as Parameters<typeof format>)) or change the writeStderr signature to accept
args: Parameters<typeof format>) so the call format(...args) is properly typed;
update the writeStderr function to use that correct assertion and keep the rest
of the logic (prefix, line, process.stderr.write) unchanged.
src/edd-client.ts (1)

296-314: Sequential API calls may cause performance issues.

getCustomerById tries up to 4 different query parameters sequentially. Combined with the 3-retry logic, this could result in up to 12 API calls in the worst case, potentially causing slowdowns or rate-limiting issues.

Consider documenting this behavior or adding a flag to limit fallback attempts when performance is critical.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/edd-client.ts` around lines 296 - 314, The getCustomerById function makes
up to four sequential API calls (each subject to the existing 3-retry logic)
which can cause high latency and rate-limit pressure; modify getCustomerById to
accept an optional parameter (e.g., tryAll?: boolean or maxAttempts?: number)
that defaults to the current behavior but when false/1 will only try the
preferred param ({ customer: customerId }) and return early on miss, or
alternatively implement parallel querying using Promise.any on the candidate
URLs (constructed via buildUrl) and then call this.request on each candidate in
parallel while preserving the existing retry wrapper; update references to
getCustomerById and add the new parameter to callers that need the
faster/limited behavior, and ensure errors are handled exactly as current
(return null if none found).
src/index.ts (1)

115-134: Deduplicate discount-code normalization.

The same normalization logic is repeated for discountCodes and discounts, and again in edd_get_sale. Extract a small helper and reuse the computed value to keep behavior consistent and reduce maintenance.

♻️ Suggested refactor
 const edd = new EDDClient(config);
 
+const normalizeDiscountCodes = (raw: unknown): string[] | null => {
+  if (Array.isArray(raw)) return raw.filter((v) => typeof v === 'string');
+  if (typeof raw === 'string' && raw.trim().length > 0) return [raw];
+  return null;
+};
+
 // Create MCP server
 const server = new McpServer({
   name: 'io.github.verygoodplugins/mcp-edd',
   version: packageJson.version,
 });
@@
-    const summary = sales.map((s) => ({
-      discountCodes: (() => {
-        const raw = (s as unknown as { discount?: unknown }).discount ?? s.discounts ?? null;
-        if (Array.isArray(raw)) return raw.filter((v) => typeof v === 'string');
-        if (typeof raw === 'string' && raw.trim().length > 0) return [raw];
-        return null;
-      })(),
+    const summary = sales.map((s) => {
+      const discountCodes = normalizeDiscountCodes(
+        (s as unknown as { discount?: unknown }).discount ?? s.discounts ?? null
+      );
+
+      return {
+        discountCodes,
       id: s.ID,
       email: s.email,
       total: s.total,
       date: s.date,
       gateway: s.gateway,
       products: s.products.map((p) => p.name),
       hasLicenses: (s.licenses?.length ?? 0) > 0,
       // Back-compat: older clients look for `discounts`.
-      discounts: (() => {
-        const raw = (s as unknown as { discount?: unknown }).discount ?? s.discounts ?? null;
-        if (Array.isArray(raw)) return raw.filter((v) => typeof v === 'string');
-        if (typeof raw === 'string' && raw.trim().length > 0) return [raw];
-        return null;
-      })(),
-    }));
+        discounts: discountCodes,
+      };
+    });
@@
-    const discountCodes = (() => {
-      const raw =
-        (sale as unknown as { discount?: unknown }).discount ?? sale.discounts ?? null;
-      if (Array.isArray(raw)) return raw.filter((v) => typeof v === 'string');
-      if (typeof raw === 'string' && raw.trim().length > 0) return [raw];
-      return null;
-    })();
+    const discountCodes = normalizeDiscountCodes(
+      (sale as unknown as { discount?: unknown }).discount ?? sale.discounts ?? null
+    );

Also applies to: 178-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 115 - 134, Duplicate discount normalization logic
appears in the object mapping for discountCodes and discounts (and again in
edd_get_sale); extract a small helper function (e.g., normalizeDiscounts or
parseDiscounts) that accepts the raw field and returns either string[] or null,
replace the inline IIFE blocks in the mapping to call that helper once and reuse
its result for both discountCodes and discounts, and update edd_get_sale to call
the same helper so all places share the same behavior.
manifest.json (1)

57-101: Consider consistent formatting in tools array.

Lines 74, 79, and 92 use inline single-line format while other entries use multi-line format. For consistency and easier diffs in future edits, consider using the same multi-line format throughout.

♻️ Optional: Consistent multi-line formatting
-    { "name": "edd_get_sale", "description": "Get complete sale details by ID or purchase key" },
+    {
+      "name": "edd_get_sale",
+      "description": "Get complete sale details by ID or purchase key"
+    },
     {
       "name": "edd_list_customers",
       "description": "List customers with purchase history and lifetime value"
     },
-    { "name": "edd_get_customer", "description": "Get customer details by ID or email address" },
+    {
+      "name": "edd_get_customer",
+      "description": "Get customer details by ID or email address"
+    },
...
-    { "name": "edd_list_discounts", "description": "List all discount codes with usage stats" },
+    {
+      "name": "edd_list_discounts",
+      "description": "List all discount codes with usage stats"
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@manifest.json` around lines 57 - 101, In manifest.json's "tools" array, some
tool entries (e.g., the objects with "name": "edd_get_sale", "name":
"edd_get_customer", and "name": "edd_get_discount") use single-line inline
formatting while the rest use multi-line objects; update those single-line
entries to the same multi-line key/value format as the other tool objects
(preserve the same "name" and "description" values and ordering) so the entire
"tools" array is consistently formatted for clearer diffs and future edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@manifest.json`:
- Around line 16-18: The manifest.json references an "icon" asset at
assets/icon.png which is missing; either add the icon file to the repository at
that path or remove the "icon": "assets/icon.png" entry from manifest.json so
the manifest no longer references a non-existent asset. Locate the "icon" key in
manifest.json (near the "screenshots" entry) and either commit a valid PNG to
assets/icon.png or delete the "icon" property from the JSON to resolve the
manifest error.

In `@tests/unit/stdio-clean.test.ts`:
- Around line 23-48: The test currently only waits 200ms then asserts stdout is
empty, which can false-pass if the spawned process exited early; capture stderr
(child.stderr) like stdout and after awaiting sleep(200) check child.exitCode
(or child.killed) and if it is not null/has exited, fail the test with the
captured stderr/exit info before asserting stdout; otherwise proceed to
expect(stdout.trim()).toBe(''). Update the test around the spawn/await block
(symbols: child, child.stdout, child.stderr, sleep, terminate) to collect stderr
and guard with an exitCode check prior to the stdout assertion.

---

Nitpick comments:
In `@manifest.json`:
- Around line 57-101: In manifest.json's "tools" array, some tool entries (e.g.,
the objects with "name": "edd_get_sale", "name": "edd_get_customer", and "name":
"edd_get_discount") use single-line inline formatting while the rest use
multi-line objects; update those single-line entries to the same multi-line
key/value format as the other tool objects (preserve the same "name" and
"description" values and ordering) so the entire "tools" array is consistently
formatted for clearer diffs and future edits.

In `@src/edd-client.ts`:
- Around line 296-314: The getCustomerById function makes up to four sequential
API calls (each subject to the existing 3-retry logic) which can cause high
latency and rate-limit pressure; modify getCustomerById to accept an optional
parameter (e.g., tryAll?: boolean or maxAttempts?: number) that defaults to the
current behavior but when false/1 will only try the preferred param ({ customer:
customerId }) and return early on miss, or alternatively implement parallel
querying using Promise.any on the candidate URLs (constructed via buildUrl) and
then call this.request on each candidate in parallel while preserving the
existing retry wrapper; update references to getCustomerById and add the new
parameter to callers that need the faster/limited behavior, and ensure errors
are handled exactly as current (return null if none found).

In `@src/env.ts`:
- Around line 42-79: Replace the manual URL validation in normalizeEddApiUrl
with a Zod schema: create an EddApiUrlSchema using z.string().url().transform
that parses the URL, enforces protocol is 'https:' or 'http:' (throwing the same
protocol error), strips search and hash, and performs the same pathname
normalization logic (edd-api prefix handling and trailing slashes) before
returning the normalized string; then refactor normalizeEddApiUrl to either use
EddApiUrlSchema.parse(rawUrl) or delegate to the schema's transform so
validation/error formatting is consistent with the project's Zod infrastructure.

In `@src/index.ts`:
- Around line 115-134: Duplicate discount normalization logic appears in the
object mapping for discountCodes and discounts (and again in edd_get_sale);
extract a small helper function (e.g., normalizeDiscounts or parseDiscounts)
that accepts the raw field and returns either string[] or null, replace the
inline IIFE blocks in the mapping to call that helper once and reuse its result
for both discountCodes and discounts, and update edd_get_sale to call the same
helper so all places share the same behavior.

In `@src/stdio.ts`:
- Around line 5-9: The cast `args as []` in writeStderr is incorrect and
misleading; instead remove that empty-tuple cast and spread args with a correct
type assertion compatible with format (e.g., use format(...(args as
Parameters<typeof format>)) or change the writeStderr signature to accept args:
Parameters<typeof format>) so the call format(...args) is properly typed; update
the writeStderr function to use that correct assertion and keep the rest of the
logic (prefix, line, process.stderr.write) unchanged.

Comment thread manifest.json
Comment thread tests/unit/stdio-clean.test.ts
- Deleted the assets/README.md file, which contained brand asset guidelines.
- Updated the stdio-clean unit test to capture and assert stderr output, ensuring better error diagnostics during process execution.
- Updated the `terminate` function to check for both `exitCode` and `signalCode` before returning.
- Refactored the promise handling for process exit to improve reliability.
- Adjusted the test timeout for better performance during execution.
@jack-arturo jack-arturo merged commit 0b26505 into main Feb 17, 2026
5 checks passed
@jack-arturo jack-arturo deleted the feat/pre-1-0-0-tests-hardening branch February 17, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant