profitlee-mcp 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LEE I HSIU
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # profitlee-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/profitlee-mcp.svg)](https://www.npmjs.com/package/profitlee-mcp)
4
+ [![license](https://img.shields.io/npm/l/profitlee-mcp.svg)](./LICENSE)
5
+
6
+ An [MCP](https://modelcontextprotocol.io) server for [Profitlee](https://profitlee.com) — compute **country-accurate Amazon FBA/FBM and TikTok Shop profit margins**, and manage saved scenarios, from any MCP client (Claude Desktop, Claude Code, Cursor, …).
7
+
8
+ `calculate_profit` is **free and needs no token**. The scenario tools require a Profitlee Pro API token.
9
+
10
+ > MCP registry name: `io.github.aronleedev/profitlee-mcp`
11
+
12
+ ## Quick start
13
+
14
+ Add to your MCP client config:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "profitlee": {
20
+ "command": "npx",
21
+ "args": ["-y", "profitlee-mcp"],
22
+ "env": {
23
+ "PROFITLEE_API_TOKEN": "eck_live_xxx"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ `PROFITLEE_API_TOKEN` is **optional** — omit the whole `env` block to use `calculate_profit` only. Create a token on your Profitlee [account page](https://profitlee.com/account) to unlock the scenario tools.
31
+
32
+ Requires Node.js 20+.
33
+
34
+ ## Tools
35
+
36
+ | Tool | Auth | Description |
37
+ | --- | --- | --- |
38
+ | `calculate_profit` | none | Full per-unit cost stack, gross/net margin, and monthly P&L. |
39
+ | `list_scenarios` | Pro token | List your saved scenarios. |
40
+ | `get_scenario` | Pro token | Read one scenario (inputs + outputs) by id. |
41
+ | `save_scenario` | Pro token | Save a named scenario from calculator inputs. |
42
+ | `update_scenario` | Pro token | Rename and/or replace a scenario's inputs. |
43
+ | `delete_scenario` | Pro token | Delete a scenario by id. |
44
+
45
+ ### `calculate_profit` inputs
46
+
47
+ Pick a `platform` + `mode`, give the product's physical and cost details, and Profitlee folds every fee into a single net margin. Rates are **0–1 decimals** (e.g. `0.15` = 15%). US uses inches + pounds; DE/JP use cm + kg.
48
+
49
+ | Field | Notes |
50
+ | --- | --- |
51
+ | `platform` | `amazon` (default) or `tiktok_shop`. |
52
+ | `region` | `us`, `de`, or `jp`. |
53
+ | `mode` | amazon: `fba` \| `fbm`. tiktok_shop: `fbt` \| `self_fulfilled`. |
54
+ | `L`, `W`, `H`, `weight` | Dimensions + unit weight. |
55
+ | `fob`, `headShip`, `duty` | Unit cost, inbound freight/unit, import duty/unit. |
56
+ | `price` | Selling price (gross; VAT-inclusive for DE/JP). |
57
+ | `ppcAcos`, `returnRate` | Ad ACoS and return rate (0–1). |
58
+ | `monthlyVolume` | Units/month (scales the P&L). |
59
+ | `referralPct` | Referral fee (0–1). Preferred over `referralCategory`. |
60
+ | `isApparel` | Affects some fees. |
61
+ | mode-specific | FBA: `inboundOption`, `storageMonths`, `storageSeason`. FBM / TikTok self-fulfilled: `outboundShipPerUnit`, `pickPackPerUnit`, `monthly3plStorage`. TikTok FBT: `storageMonthsPastFree`. |
62
+
63
+ The Profitlee API is the source of truth for validation — incomplete or out-of-range inputs come back as a clear error listing the offending fields. Full field reference: <https://profitlee.com/docs/api>.
64
+
65
+ ## Environment variables
66
+
67
+ | Var | Required | Default | Purpose |
68
+ | --- | --- | --- | --- |
69
+ | `PROFITLEE_API_TOKEN` | No | — | Pro token (`eck_live_…`); needed only for the scenario tools. |
70
+ | `PROFITLEE_BASE_URL` | No | `https://profitlee.com` | Override the API origin (testing). |
71
+
72
+ ## How it works
73
+
74
+ The server is a thin wrapper over Profitlee's public HTTP API:
75
+
76
+ - `calculate_profit` → `POST /api/v1/calculate` (public, no token).
77
+ - scenario tools → `/api/v1/scenarios*` (require the Pro token; the server fails fast with a clear message if it's missing).
78
+
79
+ No fee logic is reimplemented here, so results always match the live Profitlee calculator and current fee tables.
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ npm install
85
+ npm test # vitest (27 tests)
86
+ npm run build # tsc -> dist/
87
+ npm run dev # run from source with tsx
88
+ ```
89
+
90
+ ## Releasing
91
+
92
+ Maintainers: see [PUBLISHING.md](./PUBLISHING.md) for npm publish + MCP registry steps. The registry manifest lives in [server.json](./server.json).
93
+
94
+ ## License
95
+
96
+ [MIT](./LICENSE)
package/dist/client.js ADDED
@@ -0,0 +1,55 @@
1
+ /** Error type for every failure surfaced to the MCP caller. */
2
+ export class ProfitleeError extends Error {
3
+ }
4
+ /** Stable API `reason` codes to human messages. See https://profitlee.com/docs/api. */
5
+ const REASON_MESSAGES = {
6
+ auth_required: "Set PROFITLEE_API_TOKEN to a valid Pro API token.",
7
+ invalid_token: "PROFITLEE_API_TOKEN is not valid. Check it on your Profitlee account page.",
8
+ pro_required: "Saved scenarios require a Profitlee Pro plan.",
9
+ scenario_limit: "You've reached the saved-scenario limit. Delete one before creating another.",
10
+ invalid_input: "The calculator inputs failed validation.",
11
+ invalid_json: "The request body was not valid JSON.",
12
+ not_found: "Scenario not found.",
13
+ };
14
+ /** Abort a request that takes longer than this so a hung API can't hang the tool call. */
15
+ const REQUEST_TIMEOUT_MS = 15_000;
16
+ export async function apiRequest(config, opts) {
17
+ const headers = { "content-type": "application/json" };
18
+ if (opts.auth) {
19
+ if (!config.apiToken) {
20
+ throw new ProfitleeError("This action needs a Pro API token. Set PROFITLEE_API_TOKEN.");
21
+ }
22
+ headers.authorization = `Bearer ${config.apiToken}`;
23
+ }
24
+ let res;
25
+ try {
26
+ res = await fetch(`${config.baseUrl}${opts.path}`, {
27
+ method: opts.method,
28
+ headers,
29
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
30
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
31
+ });
32
+ }
33
+ catch (e) {
34
+ const detail = e instanceof Error ? e.message : "network error";
35
+ throw new ProfitleeError(`Could not reach Profitlee (${detail}).`);
36
+ }
37
+ const text = await res.text();
38
+ let json;
39
+ if (text) {
40
+ try {
41
+ json = JSON.parse(text);
42
+ }
43
+ catch {
44
+ // Leave undefined; HTTP status handling below still reports the failure.
45
+ }
46
+ }
47
+ if (!res.ok) {
48
+ const obj = (json ?? {});
49
+ const base = (obj.reason && REASON_MESSAGES[obj.reason]) || `Profitlee request failed (HTTP ${res.status}).`;
50
+ const limit = typeof obj.limit === "number" ? ` (limit ${obj.limit})` : "";
51
+ const issues = obj.issues ? ` Issues: ${JSON.stringify(obj.issues)}` : "";
52
+ throw new ProfitleeError(base + limit + issues);
53
+ }
54
+ return json;
55
+ }
package/dist/config.js ADDED
@@ -0,0 +1,5 @@
1
+ export function loadConfig(env = process.env) {
2
+ const baseUrl = (env.PROFITLEE_BASE_URL ?? "https://profitlee.com").replace(/\/+$/, "");
3
+ const token = env.PROFITLEE_API_TOKEN?.trim();
4
+ return { baseUrl, apiToken: token ? token : undefined };
5
+ }
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { loadConfig } from "./config.js";
5
+ import { registerCalculateTool } from "./tools/calculate.js";
6
+ import { registerScenarioTools } from "./tools/scenarios.js";
7
+ async function main() {
8
+ const config = loadConfig();
9
+ const server = new McpServer({ name: "profitlee-mcp", version: "0.1.0" });
10
+ registerCalculateTool(server, config);
11
+ registerScenarioTools(server, config);
12
+ const transport = new StdioServerTransport();
13
+ await server.connect(transport);
14
+ console.error("profitlee-mcp running on stdio");
15
+ }
16
+ main().catch((err) => {
17
+ console.error("profitlee-mcp failed to start:", err);
18
+ process.exit(1);
19
+ });
package/dist/result.js ADDED
@@ -0,0 +1,12 @@
1
+ import { ProfitleeError } from "./client.js";
2
+ /** Run an async producer; serialize success to JSON text, map errors to an error result. */
3
+ export async function toToolResult(fn) {
4
+ try {
5
+ const value = await fn();
6
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
7
+ }
8
+ catch (e) {
9
+ const msg = e instanceof ProfitleeError ? e.message : e instanceof Error ? e.message : String(e);
10
+ return { content: [{ type: "text", text: msg }], isError: true };
11
+ }
12
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Flat, richly-described shape for calculator inputs. Mode-specific fields are
4
+ * optional here; the Profitlee API remains the strict validation authority.
5
+ *
6
+ * Units: US uses inches and pounds; DE/JP use cm and kg. All rates are 0-1 decimals.
7
+ */
8
+ export const calcInputShape = {
9
+ platform: z.enum(["amazon", "tiktok_shop"]).default("amazon").describe("Sales platform. Default: amazon."),
10
+ region: z.enum(["us", "de", "jp"]).describe("Marketplace. US uses inches+pounds; DE/JP use cm+kg."),
11
+ mode: z
12
+ .enum(["fba", "fbm", "fbt", "self_fulfilled"])
13
+ .describe("Fulfillment mode. amazon: fba or fbm. tiktok_shop: fbt or self_fulfilled."),
14
+ L: z.number().positive().describe("Length. US: inches; DE/JP: cm."),
15
+ W: z.number().positive().describe("Width. US: inches; DE/JP: cm."),
16
+ H: z.number().positive().describe("Height. US: inches; DE/JP: cm."),
17
+ weight: z.number().positive().describe("Unit weight. US: pounds; DE/JP: kg."),
18
+ fob: z.number().nonnegative().describe("Unit manufacturing / FOB cost in the marketplace currency."),
19
+ headShip: z.number().nonnegative().describe("Inbound freight cost allocated per unit."),
20
+ duty: z.number().nonnegative().describe("Import duty per unit."),
21
+ price: z.number().min(0.01).describe("Selling price per unit (gross; VAT-inclusive for DE/JP)."),
22
+ ppcAcos: z.number().min(0).max(1).describe("Advertising ACoS as a 0-1 decimal (0.15 = 15%)."),
23
+ adSalesShare: z.number().min(0).max(1).optional().describe("Ad-attributed share of sales, 0-1. Omit to default to 1."),
24
+ returnRate: z.number().min(0).max(1).describe("Return rate as a 0-1 decimal (0.05 = 5%)."),
25
+ unsellableReturnRate: z
26
+ .number()
27
+ .min(0)
28
+ .max(1)
29
+ .optional()
30
+ .describe("Unsellable share of returned units, 0-1. Omit to default to 1."),
31
+ monthlyVolume: z.number().int().nonnegative().describe("Units sold per month."),
32
+ referralPct: z.number().min(0).max(1).describe("Referral/commission fee as a 0-1 decimal (0.15 = 15%). Preferred."),
33
+ referralCategory: z
34
+ .string()
35
+ .nullish()
36
+ .describe("Advanced: a category slug that overrides referralPct. Leave unset and use referralPct."),
37
+ isApparel: z.boolean().describe("Whether the product is apparel (affects some fees)."),
38
+ inboundOption: z.enum(["optimized", "partial", "single"]).optional().describe("Amazon FBA only: inbound placement option."),
39
+ storageMonths: z.number().positive().optional().describe("Amazon FBA only: number of months in storage."),
40
+ storageSeason: z.enum(["janSep", "octDec"]).optional().describe("Amazon FBA only: storage season (octDec is the Q4 peak)."),
41
+ outboundShipPerUnit: z.number().nonnegative().optional().describe("FBM or TikTok self_fulfilled: outbound shipping per unit."),
42
+ pickPackPerUnit: z.number().nonnegative().optional().describe("FBM or TikTok self_fulfilled: pick & pack per unit."),
43
+ monthly3plStorage: z.number().nonnegative().optional().describe("FBM or TikTok self_fulfilled: total monthly 3PL storage."),
44
+ storageMonthsPastFree: z
45
+ .number()
46
+ .nonnegative()
47
+ .optional()
48
+ .describe("TikTok fbt only: months stored past the 60-day free window (0 if shipped before it ends)."),
49
+ };
50
+ /** Object form for runtime parsing of calculator inputs. */
51
+ export const calcInputObject = z.object(calcInputShape);
@@ -0,0 +1,21 @@
1
+ import { apiRequest } from "../client.js";
2
+ import { toToolResult } from "../result.js";
3
+ import { calcInputShape } from "../schemas.js";
4
+ /** Core calculate call: public endpoint, no token. Returns the `result` object. */
5
+ export async function runCalculate(config, args) {
6
+ const out = await apiRequest(config, {
7
+ method: "POST",
8
+ path: "/api/v1/calculate",
9
+ body: args,
10
+ auth: false,
11
+ });
12
+ return out.result;
13
+ }
14
+ export function registerCalculateTool(server, config) {
15
+ server.registerTool("calculate_profit", {
16
+ title: "Calculate profit",
17
+ description: "Compute the full per-unit cost stack, gross/net margin, and monthly P&L for an Amazon FBA/FBM or TikTok Shop product. Free: no API token required.",
18
+ inputSchema: calcInputShape,
19
+ annotations: { readOnlyHint: true, openWorldHint: true },
20
+ }, async (args) => toToolResult(() => runCalculate(config, args)));
21
+ }
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { apiRequest, ProfitleeError } from "../client.js";
3
+ import { toToolResult } from "../result.js";
4
+ import { calcInputShape } from "../schemas.js";
5
+ const idShape = { id: z.string().min(1).describe("Scenario id.") };
6
+ export async function listScenarios(config) {
7
+ return apiRequest(config, { method: "GET", path: "/api/v1/scenarios", auth: true });
8
+ }
9
+ export async function getScenario(config, args) {
10
+ return apiRequest(config, { method: "GET", path: `/api/v1/scenarios/${encodeURIComponent(args.id)}`, auth: true });
11
+ }
12
+ export async function saveScenario(config, args) {
13
+ return apiRequest(config, {
14
+ method: "POST",
15
+ path: "/api/v1/scenarios",
16
+ auth: true,
17
+ body: { name: args.name, inputs: args.inputs },
18
+ });
19
+ }
20
+ export async function updateScenario(config, args) {
21
+ if (args.inputs !== undefined) {
22
+ const body = { inputs: args.inputs };
23
+ if (args.name !== undefined)
24
+ body.name = args.name;
25
+ return apiRequest(config, { method: "PUT", path: `/api/v1/scenarios/${encodeURIComponent(args.id)}`, auth: true, body });
26
+ }
27
+ if (args.name !== undefined) {
28
+ return apiRequest(config, {
29
+ method: "PATCH",
30
+ path: `/api/v1/scenarios/${encodeURIComponent(args.id)}`,
31
+ auth: true,
32
+ body: { name: args.name },
33
+ });
34
+ }
35
+ throw new ProfitleeError("Provide name or inputs to update.");
36
+ }
37
+ export async function deleteScenario(config, args) {
38
+ return apiRequest(config, { method: "DELETE", path: `/api/v1/scenarios/${encodeURIComponent(args.id)}`, auth: true });
39
+ }
40
+ export function registerScenarioTools(server, config) {
41
+ server.registerTool("list_scenarios", {
42
+ title: "List saved scenarios",
43
+ description: "List the caller's saved Profitlee scenarios. Requires a Pro API token.",
44
+ inputSchema: {},
45
+ annotations: { readOnlyHint: true, openWorldHint: true },
46
+ }, async () => toToolResult(() => listScenarios(config)));
47
+ server.registerTool("get_scenario", {
48
+ title: "Get a scenario",
49
+ description: "Fetch one saved scenario (inputs + outputs) by id. Requires a Pro API token.",
50
+ inputSchema: idShape,
51
+ annotations: { readOnlyHint: true, openWorldHint: true },
52
+ }, async (args) => toToolResult(() => getScenario(config, args)));
53
+ server.registerTool("save_scenario", {
54
+ title: "Save a scenario",
55
+ description: "Save a named scenario from calculator inputs. Outputs are computed server-side. Requires a Pro API token.",
56
+ inputSchema: {
57
+ name: z.string().min(1).max(120).describe("Scenario name."),
58
+ inputs: z.object(calcInputShape).describe("Calculator inputs, same shape as calculate_profit."),
59
+ },
60
+ annotations: { openWorldHint: true },
61
+ }, async (args) => toToolResult(() => saveScenario(config, args)));
62
+ server.registerTool("update_scenario", {
63
+ title: "Update a scenario",
64
+ description: "Update a saved scenario: pass inputs to replace and recompute, and/or name to rename. Requires a Pro API token.",
65
+ inputSchema: {
66
+ ...idShape,
67
+ name: z.string().min(1).max(120).optional().describe("New name (optional)."),
68
+ inputs: z.object(calcInputShape).optional().describe("Replacement calculator inputs (optional)."),
69
+ },
70
+ annotations: { openWorldHint: true },
71
+ }, async (args) => toToolResult(() => updateScenario(config, args)));
72
+ server.registerTool("delete_scenario", {
73
+ title: "Delete a scenario",
74
+ description: "Delete a saved scenario by id. Requires a Pro API token.",
75
+ inputSchema: idShape,
76
+ annotations: { destructiveHint: true, openWorldHint: true },
77
+ }, async (args) => toToolResult(() => deleteScenario(config, args)));
78
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "profitlee-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Profitlee ecommerce profit calculator (Amazon FBA/FBM + TikTok Shop).",
5
+ "license": "MIT",
6
+ "author": "AronLEE <aronleedev@gmail.com>",
7
+ "homepage": "https://github.com/AronLEEdev/profitlee-mcp#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/AronLEEdev/profitlee-mcp.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/AronLEEdev/profitlee-mcp/issues"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "modelcontextprotocol",
19
+ "amazon",
20
+ "fba",
21
+ "fbm",
22
+ "tiktok-shop",
23
+ "profit-calculator",
24
+ "margin",
25
+ "ecommerce",
26
+ "seller",
27
+ "profitlee"
28
+ ],
29
+ "mcpName": "io.github.aronleedev/profitlee-mcp",
30
+ "type": "module",
31
+ "bin": { "profitlee-mcp": "dist/index.js" },
32
+ "files": ["dist", "README.md", "LICENSE"],
33
+ "engines": { "node": ">=20" },
34
+ "publishConfig": { "access": "public" },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsx src/index.ts",
38
+ "test": "vitest run",
39
+ "prepublishOnly": "npm run build && npm test"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.12.0",
43
+ "zod": "^3.25.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "tsx": "^4.19.0",
48
+ "typescript": "^5.6.0",
49
+ "vitest": "^2.1.0"
50
+ }
51
+ }