@expander/mcp-server
v0.34.0
Published
Model Context Protocol server for the Exmachine External API
Downloads
950
Maintainers
Readme
@expander/mcp-server
Model Context Protocol (MCP) server for the Exmachine External API. Lets you query Exmachine data from Claude Desktop using your API key.
Prerequisites
- An Exmachine account with access to the data you need.
- An API key from Settings → API Keys in the Exmachine dashboard.
- Node.js 20+ (for
npx).
Claude custom connector (remote)
For claude.ai / Cowork, use the hosted MCP server with OAuth.
Add connector in Claude
- Customize → Connectors → Add custom connector
- URL:
https://mcp.exmachine.io/mcp- In a chat: + → Connectors → enable Exmachine
- Click Connect → paste your Exmachine API key (
exp_live_…from Settings → API Keys) - Ask Claude: “List my marketplaces and brands.”
OAuth endpoints (auto-discovered by Claude):
| Endpoint | Purpose |
|----------|---------|
| /.well-known/oauth-authorization-server | OAuth metadata |
| /authorize | Connect flow — API key consent page |
| /token | Token exchange |
| /register | Dynamic client registration (Claude) |
Claude Desktop setup
- Open Claude Desktop → Settings → Developer → Edit Config.
- Add the
expanderserver undermcpServers:
{
"mcpServers": {
"expander": {
"command": "npx",
"args": ["-y", "@expander/mcp-server@latest"],
"env": {
"EXMACHINE_API_KEY": "exp_live_your_key_here",
"EXMACHINE_API_BASE": "https://api.exmachine.io/api/v1"
}
}
}
}Config file paths
| OS | Path |
|----|------|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
For local development against your gateway:
"EXMACHINE_API_BASE": "http://localhost:8080/api/v1"Quit and reopen Claude Desktop after saving.
Environment variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| EXMACHINE_API_KEY | stdio mode | — | Your exp_live_… API key |
| EXMACHINE_API_BASE | No | https://api.exmachine.io/api/v1 | API root (includes /api/v1) |
| MCP_TOKEN_SECRET | HTTP mode | — | Seals OAuth tokens and client ids. See below |
| MCP_PUBLIC_URL | No | https://mcp.exmachine.io | Public origin used for OAuth metadata |
MCP_TOKEN_SECRET
HTTP mode issues OAuth tokens and client_ids that carry their own state, sealed
with AES-256-GCM under this secret — nothing about a connected client is stored
in memory, so deploys and cold starts are invisible to users.
Two consequences:
- It must be stable. Changing it invalidates every issued token and client registration; every user has to reconnect the connector.
- It must be secret. It decrypts users' Exmachine API keys. In production it
comes from Secret Manager (
mcp-token-secret), not from a plain env var.
Generate with openssl rand -hex 32. The server refuses to start without it —
falling back to a random per-boot value would silently log everyone out on every
restart, which is the bug this design removes.
Tools
Feedback
| Tool | Calls |
|------|--------|
| create_ticket | POST /external/tickets |
| list_my_tickets | GET /external/tickets |
These two are the reason the server's instructions tell Claude to offer to file a bug or feature request unprompted. The ticket is filed as the owner of the API key, lands on the team's Monday board, and shows up under Settings → My tickets in the dashboard.
Unlike every other tool here, these two paths are served by the gateway
rather than proxied to the external-api microservice — creating a ticket means
talking to Monday, and that client lives in the gateway. Key validation is still
external-api's (POST /api-keys/resolve), so there is one definition of a valid
key.
Project requests (personal tool projects)
| Tool | Calls |
|------|--------|
| request_project | POST /external/project-requests |
| my_project_requests | GET /external/project-requests |
| resend_setup_zip | POST /external/project-requests/:prq/zip |
| set_project_request_github_username | POST /external/project-requests/:prq/github |
| approve_project_request | POST /external/project-requests/:prq/approve (Management edit) |
| reject_project_request | POST /external/project-requests/:prq/reject (Management edit) |
| activate_project_request | POST /external/project-requests/:prq/activate (Management edit) |
A person asks their Claude for a tool project; the request lands in the team's
Slack channel with Approve / Reject buttons; the project-provisioner job in
database-service builds everything (GitHub repository from the kit template,
Vercel project and variables, a Neon database, the setup ZIP) and messages the
person. request_project is refused unless the person acknowledged the
monthly cost sentence (cost_acknowledged=true); it accepts no GitHub username
when the person has no account yet, and the build waits until
set_project_request_github_username is called. Design: the monorepo's
docs/prd-project-requests.md.
Catalog (discover ids first)
| Tool | Description |
|------|-------------|
| list_marketplaces | Active marketplaces → use _id as marketplace_id |
| list_brands | Brands for a marketplace → use id as brand_id |
| list_business_partners | Suppliers, forwarders, or warehouses (type param) |
Data
| Tool | Calls |
|------|--------|
| update_shipment_status | PATCH /external/shipments/{ex_number}/status |
| get_shipment_by_ex_number | GET /external/shipments/{ex_number} |
| list_shipments | GET /external/shipments |
| list_products | GET /external/products |
| list_parent_groups | GET /external/parent-groups |
| get_forecast | GET /external/forecast |
| get_targets | GET /external/targets |
| get_cash_flow_summary | GET /external/cash-flow/summary |
| list_cash_flow | GET /external/cash-flow |
| list_sellerboard | GET /external/sellerboard |
| list_fba_orders | GET /external/amazon-reports/fba-orders |
| list_data_kiosk_economics | GET /external/amazon-reports/data-kiosk-economics |
| list_data_kiosk_sales_traffic | GET /external/amazon-reports/data-kiosk-sales-traffic |
| list_data_kiosk_sales_traffic_totals | GET /external/amazon-reports/data-kiosk-sales-traffic-totals |
| … | Other Amazon report tools |
Orders, KPI Insights & fulfillment rate cards
| Tool | Calls |
|------|--------|
| list_purchase_demands | GET /external/purchase-demands |
| get_purchase_demand_by_pd_number | GET /external/purchase-demands/{pd_number} |
| list_payments | GET /external/payments |
| get_supplier_analytics | GET /external/supplier-analytics |
| list_transactions | GET /external/transactions |
| list_documents | GET /external/documents |
| get_document_content | GET /external/documents/{document_id}/content |
| get_payment_forecast | GET /external/payments/forecast |
| get_landed_cost_report | GET /external/landed-cost-report |
| list_purchase_orders | GET /external/purchase-orders |
| get_purchase_order_by_po_number | GET /external/purchase-orders/{po_number} |
| list_conversion_orders | GET /external/conversion-orders |
| get_conversion_order_by_co_number | GET /external/conversion-orders/{co_number} |
| get_kpi_insights_cards | GET /external/kpi-insights/cards |
| get_kpi_insights_drill | GET /external/kpi-insights/drill/{kpi_id} |
| get_landed_cost_grid | GET /external/kpi-insights/landed-cost-grid |
| get_kpi_insights_shipments | GET /external/kpi-insights/shipments |
| get_kpi_insights_stock_value | GET /external/kpi-insights/stock-value |
| get_maya_summary | GET /external/maya/summary |
| list_fba_fulfillment_costs | GET /external/fulfillment-costs/fba |
| list_mcf_fulfillment_costs | GET /external/fulfillment-costs/mcf |
PD/PO/CO numbers are normalized client-side (po-000901 → PO000901).
get_document_content is the one tool whose result is not JSON-as-text: a
PDF with a text layer, a spreadsheet or a text file comes back as text; a
PNG/JPEG comes back as an image block; a scanned PDF comes back as one image
block per rendered page (three per call, page continues). Long text is
windowed — the result says when it is truncated and which offset to pass to
continue. Word, TIFF and archive files are refused with a reason the user can
act on.
The four chain surfaces — purchase demands, purchase orders, conversion orders
and shipments — all accept sku plus a date_field + from + to window, so
"what is on the way for this SKU" and "what lands next month" are one call each
instead of paging every order. get_supplier_analytics rolls spend, payments
and outstanding balance up per supplier server-side, because summing order money
client-side after grouping by SKU counts an order once per line it contains.
Two tools take marketplace_id without brand_id, for different reasons:
the fulfillment rate cards are priced per marketplace, not per brand; and
list_payments is scoped that way because a payment carries no brand of its
own — the brand comes from the order it references, and is reported per row.
Most data tools require marketplace_id and brand_id. Call catalog tools first.
Archived data
Every list surface takes active: active (live only), archived (archived
only) or all. Defaults differ and each tool states its own — the catalogs and
shipments default to active, the procurement orders to all — because the
parameter was added without changing what any existing key already receives.
Archived rows matter most for history: a supplier that is no longer active is
still the supplier on last year's orders, so reading those orders back needs
active=all on list_business_partners.
Example prompts (in Claude)
- “List marketplaces and brands for Amazon.ca, then show shipments in BOOKING status.”
- “List forwarders and filter OCEAN shipments for brand Pelegon.”
- “Pull Sellerboard data for last month for marketplace X and brand Y.”
Development
cd expander-mcp
npm install
npm run build
EXMACHINE_API_KEY=exp_live_... EXMACHINE_API_BASE=http://localhost:8080/api/v1 npm startHTTP mode (Cloud Run / Claude remote connector)
npm run build
PORT=8080 \
EXMACHINE_API_BASE=https://api.exmachine.io/api/v1 \
MCP_TOKEN_SECRET=$(openssl rand -hex 32) \
npm run start:http
curl http://localhost:8080/healthMCP endpoint: POST /mcp with header Authorization: Bearer <EXMACHINE_API_KEY>.
Deploy to Google Cloud Run
Pushes to main build the Docker image and deploy expander-mcp-production to Cloud Run (europe-west1), same pattern as server_dashboard.
One-time setup
- GitHub secrets on expandertech/expander-mcp (same as other repos):
GCR_PROJECT_IDGCR_SERVICE_ACCOUNT_KEYSLACK_BOT_TOKEN(optional — deployment Slack notification)
- Artifact Registry repo
cloud-run-source-deploymust exist in GCP (already used by dashboard/gateway). - Token secret in Secret Manager — required, the container refuses to boot without it:
Create it once and never rotate casually — a new value disconnects every user.openssl rand -hex 32 | gcloud secrets create mcp-token-secret --data-file=- # let the Cloud Run runtime service account read it gcloud secrets add-iam-policy-binding mcp-token-secret \ --member="serviceAccount:$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')[email protected]" \ --role=roles/secretmanager.secretAccessor - First deploy creates the Cloud Run service. After deploy, note the
*.run.appURL from the GCP console.
Custom domain (mcp.exmachine.io)
- Cloud Run → expander-mcp-production → Manage custom domains → add
mcp.exmachine.io. - In GoDaddy DNS, add CNAME
mcp→ value from GCP (oftenghs.googlehosted.com). - Claude connector URL:
https://mcp.exmachine.io/mcp
Release flow
- Merge to
main. - GitHub Action
.github/workflows/deploy-production.ymlruns automatically. - Verify:
curl https://<your-run-url>/health
Publishing to npm
Pushes to main publish automatically when package.json version is new (already-published versions are skipped).
One-time setup
- npm account — sign up at npmjs.com (use a team account if you prefer).
- Create org
@expander— npm → avatar → Add new organization → nameexpander→ Unlimited public packages (free). - Automation token (required for CI) — npm → Access Tokens → Generate New Token:
- Type must be Automation (not Publish, not Classic).
- Automation tokens skip 2FA / OTP — required for GitHub Actions.
- If CI fails with
EOTP/ “one-time password”, delete the old secret and create a new Automation token.
- GitHub secret — repo expandertech/expander-mcp → Settings → Secrets and variables → Actions:
- Update secret
NPM_TOKENwith the Automation token (replace any Publish/Classic token).
- Update secret
- Merge workflow — commit
.github/workflows/publish-npm.ymltomain(included in this repo).
Release flow
- Bump
versioninpackage.json(e.g.0.1.0→0.1.1). - Commit, merge to
main. - GitHub Action builds and runs
npm publish. Users can thennpx @expander/mcp-server.
If you merge a change under src/ without bumping the version, the publish job
fails on purpose. Cloud Run redeploys on every push while npm only publishes
on a bump, so skipping quietly leaves Claude Desktop users on an older tool list
than the remote connector — which is exactly what happened between 0.4.0 and
0.5.0, where nine tools shipped to Cloud Run and never reached npm.
The version in package.json is the single source of truth: createServer.ts
reads it, so the version reported in the MCP handshake is always the version that
was published.
Security
- Never commit API keys. Use env vars in the Claude config only.
- The MCP server runs locally and forwards your key to the Exmachine API on each tool call.
- Rate limit: 60 requests/minute per key (same as REST).
- Dashboard permissions apply — denied endpoints return
403 forbidden.
License
MIT
