@tenpo/mcp 0.2.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 +21 -0
- package/README.md +233 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +624 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tenpo
|
|
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,233 @@
|
|
|
1
|
+
# @tenpo/mcp
|
|
2
|
+
|
|
3
|
+
> The commerce intelligence layer for any AI agent.
|
|
4
|
+
> 229 deterministic tools across orders, ads, email, inventory, suppliers, customers, finance, and competitors —
|
|
5
|
+
> exposed as a Model Context Protocol server that plugs into Claude Code, Cursor, Claude Desktop, ChatGPT, or any MCP host.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@tenpo/mcp)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx -y @tenpo/mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That's it. First run auto-issues a free API key (500 calls/day, no expiry, no card).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why Tenpo MCP
|
|
19
|
+
|
|
20
|
+
Most "AI for ecommerce" tools wrap an LLM around your store. **We don't.**
|
|
21
|
+
|
|
22
|
+
Tenpo is a **stateless commerce-ops backend** that your AI of choice calls directly. The tools return real data + grounded analysis; your AI does the reasoning and writes the response. You pay for *your* AI's tokens — Tenpo never spins up an LLM on our side, so the free tier is genuinely free.
|
|
23
|
+
|
|
24
|
+
What that means in practice:
|
|
25
|
+
- **Your AI gets factual answers**: tools execute SQL against your store, hit Klaviyo's API, ping Meta Ad Manager — never hallucinate
|
|
26
|
+
- **Multi-source joins are cheap**: ask *"true ROAS after fees"* and Tenpo joins Shopify orders × Meta spend × Stripe fees deterministically — no LLM in the join path
|
|
27
|
+
- **Latency is sub-10ms p50** for the analytics tools — they're SQL helpers, not LLM calls
|
|
28
|
+
- **Privacy is default-on**: per-merchant DuckDB, no cross-tenant leakage, k-anonymous network telemetry
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What you get
|
|
33
|
+
|
|
34
|
+
### 4 god-mode intelligence tools
|
|
35
|
+
|
|
36
|
+
| Tool | What it returns |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `tenpo_inventory_intelligence` | Working capital aging by SKU + bundle-affinity OOS impact + lead-time-adjusted reorder priorities (CRITICAL/URGENT/PLAN/OK). Sub-100ms even on large catalogs. |
|
|
39
|
+
| `tenpo_supplier_scorecard` | Per-supplier A/B/C/D/F grade combining on-time rate, lead-time variance, price drift QoQ, and lifetime spend. |
|
|
40
|
+
| `tenpo_customer_intelligence` | Predicted next-order date + churn-risk score (0–100) + top-1%/10%/25% concentration risk + cross-sell next-best-product. |
|
|
41
|
+
| `tenpo_creative_dna` | Library of 10 proven email/ad patterns + draft mode generates 3 variants with **explicit hypothesis per variant** + analyze-winners mode extracts patterns from your past top performers. |
|
|
42
|
+
|
|
43
|
+
### 25 commerce primitives (always loaded)
|
|
44
|
+
|
|
45
|
+
Storefront, orders, products, customers, segments, suppliers, POs, ad spend, email campaigns, charts, drafts, approvals, integrations connect/disconnect/sync, workflows, web search, SQL queries.
|
|
46
|
+
|
|
47
|
+
### ~200 specialist tools (loaded on demand by your AI)
|
|
48
|
+
|
|
49
|
+
Ad-spend pause, PO generation + email, refund processing, abandoned-cart flows, Shopify GraphQL/Admin, Meta/Google/TikTok ads, Klaviyo campaign create + execute, Gorgias support reply, bulk Shopify operations, custom-platform adapters, more.
|
|
50
|
+
|
|
51
|
+
### Network intelligence
|
|
52
|
+
|
|
53
|
+
`tenpo_network_intelligence` exposes 6 sections of **k-anonymous (k≥3 merchants)** signals from across the network — top intents merchants ask, tool health (call-count, p50/p95 latency, error rate), per-detector signal/noise grades, creative pattern effectiveness (drafts → launches → revenue), and peer benchmarks (`category × metric` median/p20/p80).
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
### Claude Code (CLI)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude mcp add tenpo --command "npx -y @tenpo/mcp"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Claude Desktop
|
|
66
|
+
|
|
67
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"tenpo": {
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "@tenpo/mcp"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Restart Claude Desktop. The first call auto-issues a free key and prints it to your terminal — save it to `env.TENPO_API_KEY` to keep using the same key permanently:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"tenpo": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "@tenpo/mcp"],
|
|
88
|
+
"env": { "TENPO_API_KEY": "tnp_live_..." }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Cursor
|
|
95
|
+
|
|
96
|
+
Settings → MCP → Add new global MCP server:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"tenpo": {
|
|
102
|
+
"command": "npx",
|
|
103
|
+
"args": ["-y", "@tenpo/mcp"],
|
|
104
|
+
"env": { "TENPO_API_KEY": "tnp_live_..." }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Codex / Hermes / OpenClaw / any MCP-compatible client
|
|
111
|
+
|
|
112
|
+
Standard stdio transport — point the client at `npx -y @tenpo/mcp` with `TENPO_API_KEY` in env.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Connecting your store
|
|
117
|
+
|
|
118
|
+
The first time your AI calls a data tool, Tenpo's onboarding flow walks you through connecting your store. Just say:
|
|
119
|
+
|
|
120
|
+
> *"connect my Shopify store"*
|
|
121
|
+
|
|
122
|
+
…and the agent walks you through paste-the-token (Shopify, WooCommerce) or OAuth (Amazon, Etsy). Other platforms supported via lazy-mode adapters: **Squarespace, Wix, BigCommerce, Magento (Adobe Commerce), Salesforce Commerce Cloud, Webflow**, plus CSV/JSON import for any platform.
|
|
123
|
+
|
|
124
|
+
After the store connects, you can layer in Klaviyo / Mailchimp / Omnisend (email), Meta Ads / Google Ads / TikTok Ads (paid), Google Analytics 4 (attribution), Stripe (payments), and 40+ others.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Pricing
|
|
129
|
+
|
|
130
|
+
| Tier | Daily call limit | Cost | What you get |
|
|
131
|
+
|---|---|---|---|
|
|
132
|
+
| **Free** | 500 calls/day | $0 | Full tool access, 1 connected store, Klaviyo + Meta + GA4, no expiry |
|
|
133
|
+
| **Pro Monthly** | 5,000 calls/day | $19/mo | Everything in Free + higher limits, priority support |
|
|
134
|
+
| **Pro Yearly** | Unlimited | $190/yr | Everything in Pro Monthly, no daily cap, save $38 |
|
|
135
|
+
|
|
136
|
+
Your same API key keeps working after upgrade — the limit just changes server-side.
|
|
137
|
+
|
|
138
|
+
When you hit a free-tier limit, every response includes an `upgrade_url` pointing at a hosted Dodo Payments checkout. Pay → tier auto-bumps via webhook → you're on Pro within seconds.
|
|
139
|
+
|
|
140
|
+
**You always pay for your AI's tokens.** Tenpo runs zero LLMs server-side — every analytical tool is deterministic SQL or HTTP, sub-10ms p50.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Architecture
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
148
|
+
│ Claude Code / Cursor / Claude Desktop / Codex / your MCP host │
|
|
149
|
+
└──────────────────────┬───────────────────────────────────────────┘
|
|
150
|
+
│ stdio (MCP protocol, JSON-RPC)
|
|
151
|
+
▼
|
|
152
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
153
|
+
│ @tenpo/mcp (this package — ~5 KB, no LLM, pure proxy) │
|
|
154
|
+
└──────────────────────┬───────────────────────────────────────────┘
|
|
155
|
+
│ HTTPS, Bearer tnp_live_*
|
|
156
|
+
▼
|
|
157
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
158
|
+
│ api.tenpo.ai (gateway + runtime — Tenpo's hosted backend) │
|
|
159
|
+
│ ├─ 229 deterministic tools (SQL helpers + HTTP integrations) │
|
|
160
|
+
│ ├─ Per-merchant DuckDB (your store data, encrypted) │
|
|
161
|
+
│ ├─ Background syncs (Klaviyo, Meta, GA4, Stripe — every 15min) │
|
|
162
|
+
│ ├─ Heartbeat detectors (revenue cliff, stockout, refund spike) │
|
|
163
|
+
│ └─ K-anonymous network telemetry (Supabase, retained 90d) │
|
|
164
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Your AI does the synthesis. Tenpo provides the synapses.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Privacy
|
|
172
|
+
|
|
173
|
+
- **Per-merchant DuckDB**: every merchant gets their own database file. No cross-tenant SQL.
|
|
174
|
+
- **Salt-hashed merchant IDs** in network telemetry: `sha256(salt + merchant_id)`. Irreversible.
|
|
175
|
+
- **PII strip on free text**: emails, phone numbers, $ amounts, IDs, and API tokens are replaced with placeholders before any cross-merchant aggregation.
|
|
176
|
+
- **K-anonymity (k≥3)** enforced on every read API: stats only surface when ≥3 distinct merchants contributed.
|
|
177
|
+
- **Retention**: raw events 90 days, aggregates indefinitely. Retention cron runs daily in Postgres.
|
|
178
|
+
- **No LLM in the analytical path**: your queries don't get fed to any model — Tenpo just runs SQL and returns rows.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Examples
|
|
183
|
+
|
|
184
|
+
Once installed, ask your AI host things like:
|
|
185
|
+
|
|
186
|
+
> *"Why is revenue down today?"*
|
|
187
|
+
> → calls `tenpo_revenue_trend`, `tenpo_inventory_intelligence`, `tenpo_top_products`; AI synthesizes a one-paragraph answer with the actual numbers
|
|
188
|
+
|
|
189
|
+
> *"Draft a winback campaign for lapsed champions"*
|
|
190
|
+
> → calls `tenpo_creative_dna(mode: draft, goal: winback)` → returns 3 variants with explicit hypotheses → AI fills in the copy → optional: `tenpo_create_campaign(pattern_id: ...)` to launch it via Klaviyo
|
|
191
|
+
|
|
192
|
+
> *"Which suppliers should I renegotiate with?"*
|
|
193
|
+
> → calls `tenpo_supplier_scorecard` → AI ranks D/F-graded suppliers and drafts an opening email via `tenpo_draft_supplier_email`
|
|
194
|
+
|
|
195
|
+
> *"Which competitor changed prices this week?"*
|
|
196
|
+
> → calls `tenpo_competitor_intel(brand: "Allbirds")` → first call scrapes live + caches 24h → returns price-change diff
|
|
197
|
+
|
|
198
|
+
> *"How am I doing vs other apparel stores my size?"*
|
|
199
|
+
> → calls `tenpo_network_intelligence(section: benchmark, category: apparel, metric: aov)` → returns peer median/p20/p80 (k-anonymous, ≥3 merchants required)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/tenpo-ai/tenpo-mcp
|
|
207
|
+
cd mcp
|
|
208
|
+
npm install
|
|
209
|
+
npm run build
|
|
210
|
+
TENPO_API_URL=http://localhost:3199 TENPO_API_KEY=tnp_live_... node dist/index.js
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The MCP package itself is ~200 lines of TypeScript — a thin stdio↔HTTPS proxy. All the intelligence lives at `api.tenpo.ai`.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Status
|
|
218
|
+
|
|
219
|
+
- Production-grade, running live at [api.tenpo.ai](https://api.tenpo.ai)
|
|
220
|
+
- 229 tools exposed, 6 telemetry signal types, k-anonymous network intelligence
|
|
221
|
+
- Sub-10ms p50 latency on analytical tools (verified live on multi-GB merchant data)
|
|
222
|
+
- Standard Webhooks signature verification + idempotency on billing webhooks
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT — fork freely. The MCP package is a thin client; the heavy lifting happens on Tenpo's hosted backend.
|
|
227
|
+
|
|
228
|
+
## Links
|
|
229
|
+
|
|
230
|
+
- **Website**: [tenpo.ai](https://tenpo.ai)
|
|
231
|
+
- **API base**: [api.tenpo.ai](https://api.tenpo.ai)
|
|
232
|
+
- **Issues**: [github.com/tenpo-ai/tenpo-mcp/issues](https://github.com/tenpo-ai/tenpo-mcp/issues)
|
|
233
|
+
- **Source**: [github.com/tenpo-ai/tenpo-mcp](https://github.com/tenpo-ai/tenpo-mcp)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @tenpo/mcp — Tenpo MCP server (universal: works with any MCP client)
|
|
4
|
+
*
|
|
5
|
+
* A thin stdio MCP wrapper that translates MCP protocol calls into HTTP
|
|
6
|
+
* calls against Tenpo's /api/v1/* endpoints.
|
|
7
|
+
*
|
|
8
|
+
* Compatible with ANY MCP client:
|
|
9
|
+
* • Coding agents: Claude Code, Cursor, Codex, Cline, Aider, Continue,
|
|
10
|
+
* Windsurf, Zed, Cody, Roo Code, Open Interpreter, Goose
|
|
11
|
+
* • Chat clients: Claude Desktop, ChatGPT (when MCP lands), Open WebUI,
|
|
12
|
+
* LibreChat, generic stdio MCP hosts
|
|
13
|
+
* • Whatever LLM: Opus, Sonnet, GPT-4/5, Gemini, DeepSeek, Qwen, Llama,
|
|
14
|
+
* Mistral — host AI does the reasoning, Tenpo provides
|
|
15
|
+
* the data + tools + skills + memory.
|
|
16
|
+
*
|
|
17
|
+
* Tenpo NEVER runs an LLM. Free tier ≈ 200 messages/month (50 MCP calls/day).
|
|
18
|
+
* Trial: 7 days, ~30 calls/day. Higher limits available on paid tiers.
|
|
19
|
+
*
|
|
20
|
+
* The npm package is ~5KB. ALL the intelligence lives on Tenpo's VPS.
|
|
21
|
+
*
|
|
22
|
+
* Setup (in any MCP client config):
|
|
23
|
+
* {
|
|
24
|
+
* "mcpServers": {
|
|
25
|
+
* "tenpo": {
|
|
26
|
+
* "command": "npx",
|
|
27
|
+
* "args": ["-y", "@tenpo/mcp"],
|
|
28
|
+
* "env": { "TENPO_API_KEY": "tnp_live_..." }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* Or for trial (no API key — auto-issued):
|
|
34
|
+
* { "command": "npx", "args": ["-y", "@tenpo/mcp"] }
|
|
35
|
+
*/
|
|
36
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @tenpo/mcp — Tenpo MCP server (universal: works with any MCP client)
|
|
4
|
+
*
|
|
5
|
+
* A thin stdio MCP wrapper that translates MCP protocol calls into HTTP
|
|
6
|
+
* calls against Tenpo's /api/v1/* endpoints.
|
|
7
|
+
*
|
|
8
|
+
* Compatible with ANY MCP client:
|
|
9
|
+
* • Coding agents: Claude Code, Cursor, Codex, Cline, Aider, Continue,
|
|
10
|
+
* Windsurf, Zed, Cody, Roo Code, Open Interpreter, Goose
|
|
11
|
+
* • Chat clients: Claude Desktop, ChatGPT (when MCP lands), Open WebUI,
|
|
12
|
+
* LibreChat, generic stdio MCP hosts
|
|
13
|
+
* • Whatever LLM: Opus, Sonnet, GPT-4/5, Gemini, DeepSeek, Qwen, Llama,
|
|
14
|
+
* Mistral — host AI does the reasoning, Tenpo provides
|
|
15
|
+
* the data + tools + skills + memory.
|
|
16
|
+
*
|
|
17
|
+
* Tenpo NEVER runs an LLM. Free tier ≈ 200 messages/month (50 MCP calls/day).
|
|
18
|
+
* Trial: 7 days, ~30 calls/day. Higher limits available on paid tiers.
|
|
19
|
+
*
|
|
20
|
+
* The npm package is ~5KB. ALL the intelligence lives on Tenpo's VPS.
|
|
21
|
+
*
|
|
22
|
+
* Setup (in any MCP client config):
|
|
23
|
+
* {
|
|
24
|
+
* "mcpServers": {
|
|
25
|
+
* "tenpo": {
|
|
26
|
+
* "command": "npx",
|
|
27
|
+
* "args": ["-y", "@tenpo/mcp"],
|
|
28
|
+
* "env": { "TENPO_API_KEY": "tnp_live_..." }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* Or for trial (no API key — auto-issued):
|
|
34
|
+
* { "command": "npx", "args": ["-y", "@tenpo/mcp"] }
|
|
35
|
+
*/
|
|
36
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
37
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
38
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, CreateMessageResultSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
39
|
+
const TENPO_API_BASE = process.env.TENPO_API_URL ?? "https://api.tenpo.ai";
|
|
40
|
+
let TENPO_API_KEY = process.env.TENPO_API_KEY ?? "";
|
|
41
|
+
const log = (...args) => console.error("[@tenpo/mcp]", ...args);
|
|
42
|
+
// ── HTTP helper ──────────────────────────────────────────────────────────────
|
|
43
|
+
async function tenpoFetch(path, options = {}) {
|
|
44
|
+
const url = `${TENPO_API_BASE}${path}`;
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
method: options.method ?? "GET",
|
|
47
|
+
headers: {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
...(TENPO_API_KEY ? { Authorization: `Bearer ${TENPO_API_KEY}` } : {}),
|
|
50
|
+
"User-Agent": "@tenpo/mcp/0.1.0",
|
|
51
|
+
},
|
|
52
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
53
|
+
signal: AbortSignal.timeout(180_000),
|
|
54
|
+
});
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
let json;
|
|
57
|
+
try {
|
|
58
|
+
json = JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
json = { raw: text };
|
|
62
|
+
}
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const errBody = json;
|
|
65
|
+
throw new Error(`Tenpo API error ${res.status}: ${errBody.error ?? text.slice(0, 200)}`);
|
|
66
|
+
}
|
|
67
|
+
return json;
|
|
68
|
+
}
|
|
69
|
+
// ── First-run trial-key issuance ─────────────────────────────────────────────
|
|
70
|
+
async function ensureApiKey() {
|
|
71
|
+
if (TENPO_API_KEY)
|
|
72
|
+
return;
|
|
73
|
+
log("No TENPO_API_KEY set. Issuing free-tier key (500 calls/day, no expiry).");
|
|
74
|
+
try {
|
|
75
|
+
// /api/v1/connect/issue-key is the current name; backend also accepts the
|
|
76
|
+
// legacy /trial-key for back-compat with older MCP package versions.
|
|
77
|
+
const res = (await tenpoFetch("/api/v1/connect/issue-key", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: { client_hint: process.env.npm_package_name ?? "mcp-stdio" },
|
|
80
|
+
}));
|
|
81
|
+
if (res.api_key) {
|
|
82
|
+
TENPO_API_KEY = res.api_key;
|
|
83
|
+
log(`\n` +
|
|
84
|
+
`╔══════════════════════════════════════════════════════════════════════╗\n` +
|
|
85
|
+
`║ Tenpo MCP — Free Tier API Key Issued ║\n` +
|
|
86
|
+
`╠══════════════════════════════════════════════════════════════════════╣\n` +
|
|
87
|
+
`║ Save this in your MCP config to keep using the same key: ║\n` +
|
|
88
|
+
`║ ║\n` +
|
|
89
|
+
`║ TENPO_API_KEY=${(res.api_key ?? "").padEnd(54)}║\n` +
|
|
90
|
+
`║ ║\n` +
|
|
91
|
+
`║ Free tier: ${(res.daily_call_limit ?? 500).toString().padEnd(4)} calls/day, no expiry.${" ".repeat(28)}║\n` +
|
|
92
|
+
`║ Upgrade for higher limits: ${(res.upgrade_url ?? "https://tenpo.ai/billing/upgrade").padEnd(42)}║\n` +
|
|
93
|
+
`║ Connect your store: ${(res.connect_url ?? "https://tenpo.ai/connect").padEnd(42)}║\n` +
|
|
94
|
+
`╚══════════════════════════════════════════════════════════════════════╝\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
log(`Failed to issue API key: ${String(err).slice(0, 120)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ── MCP server ──────────────────────────────────────────────────────────────
|
|
102
|
+
const server = new Server({
|
|
103
|
+
name: "tenpo",
|
|
104
|
+
version: "0.2.0",
|
|
105
|
+
}, {
|
|
106
|
+
capabilities: {
|
|
107
|
+
tools: {},
|
|
108
|
+
resources: {},
|
|
109
|
+
prompts: {},
|
|
110
|
+
// NOTE: sampling is a CLIENT capability (declared by the host, not the
|
|
111
|
+
// server). We just call sampling/createMessage — supported clients (Claude
|
|
112
|
+
// Desktop / Claude Code) handle it, others return method-not-found and
|
|
113
|
+
// we fall back to BM25 gracefully.
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
// ── Sampling capability detection ───────────────────────────────────────────
|
|
117
|
+
// Some hosts (Claude Desktop, Claude Code) support sampling. Others don't yet.
|
|
118
|
+
// Attempt detection by checking client capabilities after handshake.
|
|
119
|
+
let CLIENT_SUPPORTS_SAMPLING = null;
|
|
120
|
+
async function tryClassifyIntentViaSampling(query, taxonomy) {
|
|
121
|
+
if (CLIENT_SUPPORTS_SAMPLING === false)
|
|
122
|
+
return null;
|
|
123
|
+
try {
|
|
124
|
+
const result = await server.request({
|
|
125
|
+
method: "sampling/createMessage",
|
|
126
|
+
params: {
|
|
127
|
+
messages: [
|
|
128
|
+
{
|
|
129
|
+
role: "user",
|
|
130
|
+
content: {
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `${taxonomy}\n\nMERCHANT QUERY: ${query.slice(0, 400)}\n\nReply with ONLY the JSON: {"intent":"<intent_id>","confidence":0.0-1.0,"reason":"one short sentence"}`,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
maxTokens: 200,
|
|
137
|
+
temperature: 0,
|
|
138
|
+
systemPrompt: "You are a deterministic intent classifier. Read the taxonomy. Pick the single best intent_id for the query. Reply with valid JSON only — no prose, no fences.",
|
|
139
|
+
modelPreferences: {
|
|
140
|
+
// Cheap, fast, accurate — host AI picks the model it has available
|
|
141
|
+
speedPriority: 0.7,
|
|
142
|
+
costPriority: 0.7,
|
|
143
|
+
intelligencePriority: 0.5,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}, CreateMessageResultSchema);
|
|
147
|
+
CLIENT_SUPPORTS_SAMPLING = true;
|
|
148
|
+
const text = result?.content?.text ?? "";
|
|
149
|
+
const m = text.match(/\{[\s\S]*?\}/);
|
|
150
|
+
if (!m)
|
|
151
|
+
return null;
|
|
152
|
+
return m[0];
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const errMsg = String(err);
|
|
156
|
+
// Method-not-found = host doesn't support sampling. Cache the negative.
|
|
157
|
+
if (/-32601|method.*not.*found|sampling/i.test(errMsg)) {
|
|
158
|
+
CLIENT_SUPPORTS_SAMPLING = false;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── Tools ───────────────────────────────────────────────────────────────────
|
|
164
|
+
const TOOLS = [
|
|
165
|
+
{
|
|
166
|
+
name: "tenpo_route",
|
|
167
|
+
description: "PREFLIGHT INTELLIGENCE — Returns a curated tool/skill bundle for a query. Tenpo NEVER runs an LLM; you (the host) do the synthesis with your own tokens. Returns: relevant_tools (top 5 of 175+), relevant_skills (top 3 of 199 with content), suggested_path, pre_executed_data (revenue/alerts), suggested_next_actions, soul. Call this once per user query, then tenpo_run_tool for each tool you need.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
required: ["query"],
|
|
171
|
+
properties: {
|
|
172
|
+
query: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "What the merchant wants to know or do, in natural language",
|
|
175
|
+
},
|
|
176
|
+
top_tools: { type: "number", description: "How many tools to return (default 5)" },
|
|
177
|
+
top_skills: { type: "number", description: "How many skills to return (default 3)" },
|
|
178
|
+
skip_pre_exec: {
|
|
179
|
+
type: "boolean",
|
|
180
|
+
description: "Skip pre-executing SQL data (faster, less context)",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "tenpo_run_tool",
|
|
187
|
+
description: "Execute any of Tenpo's 175+ specific tools by name. Use after tenpo_route or tenpo_overview shows you which tool fits the task. Examples: tenpo_query (raw SQL on merchant DB), top_customers, low_stock, revenue_trend, tenpo_fetch_url (free webpage fetch — markdown/text/html), tenpo_create_workflow, tenpo_meta_ads, send_email, etc. Pass merchantId in args.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
required: ["tool", "args"],
|
|
191
|
+
properties: {
|
|
192
|
+
tool: { type: "string", description: "Tenpo tool name (e.g. 'tenpo_query', 'top_customers')" },
|
|
193
|
+
args: {
|
|
194
|
+
type: "object",
|
|
195
|
+
description: "Tool-specific arguments. merchantId is auto-injected.",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "tenpo_classify_intent",
|
|
202
|
+
description: "INTENT CLASSIFIER — classifies a merchant query into one of Tenpo's intents (connect / investigate / create / launch / pause / discover / memory / onboard / strategy / global_signal / etc.) and returns the routing bundle for that intent. Uses MCP sampling (your host LLM) to do the classification — Tenpo never calls a paid LLM. Returns: classified_intent, confidence, matched_tools, matched_skills, suggested_path. Call this BEFORE tenpo_run_tool when the user query is ambiguous or compound (multiple intents). For clear single-intent queries, you can skip this and pick directly from tools/list.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
required: ["query"],
|
|
206
|
+
properties: {
|
|
207
|
+
query: { type: "string", description: "The merchant's query in natural language" },
|
|
208
|
+
top_tools: { type: "number", description: "Max tools to return (default 5)" },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "tenpo_get_resource",
|
|
214
|
+
description: "Read a Tenpo resource. soul (5 always-on skills), machine_rules (concise operating rules), playbook (operational scenarios), user_md (per-merchant context files + last 7 daily memos), cross_session_memory, memory, graduated_routines, schema (DuckDB), integrations, recent_alerts, intelligence_snapshot (heartbeat data), sync_status, autonomous_incidents, action_patterns, skills_index, routines.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
required: ["resource"],
|
|
218
|
+
properties: {
|
|
219
|
+
resource: {
|
|
220
|
+
type: "string",
|
|
221
|
+
enum: [
|
|
222
|
+
"soul",
|
|
223
|
+
"machine_rules",
|
|
224
|
+
"playbook",
|
|
225
|
+
"cross_session_memory",
|
|
226
|
+
"user_md",
|
|
227
|
+
"graduated_routines",
|
|
228
|
+
"memory",
|
|
229
|
+
"schema",
|
|
230
|
+
"integrations",
|
|
231
|
+
"recent_alerts",
|
|
232
|
+
"intelligence_snapshot",
|
|
233
|
+
"sync_status",
|
|
234
|
+
"autonomous_incidents",
|
|
235
|
+
"action_patterns",
|
|
236
|
+
"skills_index",
|
|
237
|
+
"routines",
|
|
238
|
+
"recommended_mcps",
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
// tenpo_route_skills DROPPED — read tenpo://skills_index, pick by judgment
|
|
245
|
+
// tenpo_overview DROPPED — use ListToolsRequestSchema/ListResourcesRequestSchema
|
|
246
|
+
{
|
|
247
|
+
name: "tenpo_get_prompt",
|
|
248
|
+
description: "Get one of Tenpo's 199 expert skill playbooks (refund response, ad audit, supplier email, etc.) as a callable prompt template. Use when the host AI faces a domain-specific task and wants Tenpo's playbook.",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
required: ["id"],
|
|
252
|
+
properties: {
|
|
253
|
+
id: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description: "Skill ID (e.g. 'tenpo-crm', '40rty-routines-morning-store-briefing')",
|
|
256
|
+
},
|
|
257
|
+
variables: {
|
|
258
|
+
type: "object",
|
|
259
|
+
description: "Optional variables to interpolate ({{customer_id}}, etc.)",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "tenpo_list_prompts",
|
|
266
|
+
description: "List all 199 available Tenpo skill playbooks. Optionally filter by search query.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
q: { type: "string", description: "Search query (filters by name/description)" },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "tenpo_connect_integration",
|
|
276
|
+
description: "Connect a new integration via API key (no OAuth — chat-only flow). Step 1: call with {integration_id} to get instructions. Step 2: call with {integration_id, credentials} to save & sync. Supported: shopify, klaviyo, meta_ads, google_ads, stripe, gmail, telegram, slack, particl, custom_http, +20 more.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: "object",
|
|
279
|
+
required: ["integration_id"],
|
|
280
|
+
properties: {
|
|
281
|
+
integration_id: {
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "Integration to connect (e.g. 'shopify', 'klaviyo')",
|
|
284
|
+
},
|
|
285
|
+
credentials: {
|
|
286
|
+
type: "object",
|
|
287
|
+
description: "Optional — if omitted, returns instructions. Otherwise saves the credentials.",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "tenpo_remember",
|
|
294
|
+
description: "Save context to per-merchant memory. scope='fact'|'preference'|'decision'|'terminology' writes to the semantic-searchable DB; scope='user_md'|'memory_md'|'context_md'|'profile_md' appends/replaces the per-merchant markdown files. Cross-session, durable.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
required: ["content"],
|
|
298
|
+
properties: {
|
|
299
|
+
content: { type: "string", description: "What to remember (3-2000 chars for DB, up to 50,000 for markdown)" },
|
|
300
|
+
scope: {
|
|
301
|
+
type: "string",
|
|
302
|
+
enum: ["fact", "preference", "decision", "terminology", "user_md", "memory_md", "context_md", "profile_md"],
|
|
303
|
+
description: "Storage scope (default: fact)",
|
|
304
|
+
},
|
|
305
|
+
mode: {
|
|
306
|
+
type: "string",
|
|
307
|
+
enum: ["append", "prepend", "replace"],
|
|
308
|
+
description: "For markdown scopes only (default: append)",
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
// DROPPED: tenpo_pattern_detect (host runs SQL via tenpo_query, detects itself),
|
|
314
|
+
// tenpo_fetch_url (host has WebFetch/@web/browser),
|
|
315
|
+
// tenpo_write_md (merged into tenpo_remember).
|
|
316
|
+
];
|
|
317
|
+
let DYNAMIC_TOOLS_CACHE = null;
|
|
318
|
+
const DYNAMIC_TOOLS_TTL_MS = 10 * 60 * 1000;
|
|
319
|
+
async function loadDynamicTools() {
|
|
320
|
+
// Cache check
|
|
321
|
+
if (DYNAMIC_TOOLS_CACHE &&
|
|
322
|
+
Date.now() - DYNAMIC_TOOLS_CACHE.fetchedAt < DYNAMIC_TOOLS_TTL_MS) {
|
|
323
|
+
return DYNAMIC_TOOLS_CACHE.tools;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const result = (await tenpoFetch("/api/v1/tools?include_schema=1&include_descriptions=1"));
|
|
327
|
+
// Merge gateway tools + meta-only tools the runtime advertises (e.g.
|
|
328
|
+
// tenpo_winning_patterns, tenpo_competitor_*) — they only exist as MCP
|
|
329
|
+
// tools, not in the gateway-tools registry, but are callable.
|
|
330
|
+
const all = [
|
|
331
|
+
...(result.tools ?? []),
|
|
332
|
+
...(result.meta_tools ?? []),
|
|
333
|
+
];
|
|
334
|
+
// Dedupe by name (gateway tools take precedence — they have schemas)
|
|
335
|
+
const seen = new Set();
|
|
336
|
+
const deduped = [];
|
|
337
|
+
for (const t of all) {
|
|
338
|
+
if (!t?.name || seen.has(t.name))
|
|
339
|
+
continue;
|
|
340
|
+
seen.add(t.name);
|
|
341
|
+
deduped.push(t);
|
|
342
|
+
}
|
|
343
|
+
DYNAMIC_TOOLS_CACHE = { fetchedAt: Date.now(), tools: deduped };
|
|
344
|
+
return deduped;
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
log(`[mcp] failed to load dynamic tools: ${String(err).slice(0, 120)}`);
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
352
|
+
await ensureApiKey();
|
|
353
|
+
const dynamic = await loadDynamicTools();
|
|
354
|
+
// Filter out names that collide with our 7 meta-tools — those have richer
|
|
355
|
+
// descriptions tailored to host-AI ergonomics, so we want ours to win.
|
|
356
|
+
const metaNames = new Set(TOOLS.map((t) => t.name));
|
|
357
|
+
const dynamicEntries = dynamic
|
|
358
|
+
.filter((t) => !metaNames.has(t.name))
|
|
359
|
+
.map((t) => ({
|
|
360
|
+
name: t.name,
|
|
361
|
+
description: t.description ?? `Tenpo tool: ${t.name}`,
|
|
362
|
+
// Default to permissive object schema if backend didn't include one
|
|
363
|
+
// (some meta-only tools advertise themselves without schemas).
|
|
364
|
+
inputSchema: t.inputSchema ?? {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: {},
|
|
367
|
+
},
|
|
368
|
+
}));
|
|
369
|
+
return { tools: [...TOOLS, ...dynamicEntries] };
|
|
370
|
+
});
|
|
371
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
372
|
+
await ensureApiKey();
|
|
373
|
+
const { name, arguments: args = {} } = request.params;
|
|
374
|
+
try {
|
|
375
|
+
switch (name) {
|
|
376
|
+
case "tenpo_route": {
|
|
377
|
+
const result = await tenpoFetch("/api/v1/route", {
|
|
378
|
+
method: "POST",
|
|
379
|
+
body: args,
|
|
380
|
+
});
|
|
381
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
382
|
+
}
|
|
383
|
+
case "tenpo_run_tool": {
|
|
384
|
+
const a = args;
|
|
385
|
+
if (!a.tool)
|
|
386
|
+
throw new Error("tool name required");
|
|
387
|
+
const result = await tenpoFetch(`/api/v1/tools/${encodeURIComponent(a.tool)}`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
body: a.args ?? {},
|
|
390
|
+
});
|
|
391
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
392
|
+
}
|
|
393
|
+
case "tenpo_classify_intent": {
|
|
394
|
+
const a = args;
|
|
395
|
+
if (!a.query)
|
|
396
|
+
throw new Error("query required");
|
|
397
|
+
// 1. Fetch the intent taxonomy from Tenpo VPS (cached server-side)
|
|
398
|
+
const taxonomy = await tenpoFetch("/api/v1/resources/intent_taxonomy");
|
|
399
|
+
const taxonomyText = taxonomy?.data?.taxonomy_prompt ?? "";
|
|
400
|
+
// 2. Use MCP sampling to ask the HOST LLM to classify against the taxonomy.
|
|
401
|
+
// Host AI does the LLM call — Tenpo pays nothing.
|
|
402
|
+
let classifiedIntent = null;
|
|
403
|
+
let confidence = 0;
|
|
404
|
+
let reason = "";
|
|
405
|
+
const sampled = await tryClassifyIntentViaSampling(a.query, taxonomyText);
|
|
406
|
+
if (sampled) {
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(sampled);
|
|
409
|
+
classifiedIntent = parsed.intent ?? null;
|
|
410
|
+
confidence = Number(parsed.confidence ?? 0);
|
|
411
|
+
reason = String(parsed.reason ?? "");
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
/* parse failure — fall through to BM25 path */
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// 3. Pass the classified intent to Tenpo VPS so it can route precisely.
|
|
418
|
+
// If sampling wasn't supported / failed, the VPS falls back to BM25.
|
|
419
|
+
const result = await tenpoFetch("/api/v1/route", {
|
|
420
|
+
method: "POST",
|
|
421
|
+
body: {
|
|
422
|
+
query: a.query,
|
|
423
|
+
top_tools: a.top_tools ?? 5,
|
|
424
|
+
top_skills: 4,
|
|
425
|
+
classified_intent: classifiedIntent,
|
|
426
|
+
classification_confidence: confidence,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
content: [{
|
|
431
|
+
type: "text",
|
|
432
|
+
text: JSON.stringify({
|
|
433
|
+
...result,
|
|
434
|
+
classification: {
|
|
435
|
+
intent: classifiedIntent,
|
|
436
|
+
confidence,
|
|
437
|
+
reason,
|
|
438
|
+
method: classifiedIntent ? "host_llm_sampling" : "bm25_fallback",
|
|
439
|
+
client_supports_sampling: CLIENT_SUPPORTS_SAMPLING,
|
|
440
|
+
},
|
|
441
|
+
}, null, 2),
|
|
442
|
+
}],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
case "tenpo_get_resource": {
|
|
446
|
+
const a = args;
|
|
447
|
+
if (!a.resource)
|
|
448
|
+
throw new Error("resource name required");
|
|
449
|
+
const result = await tenpoFetch(`/api/v1/resources/${encodeURIComponent(a.resource)}`);
|
|
450
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
451
|
+
}
|
|
452
|
+
case "tenpo_get_prompt": {
|
|
453
|
+
const a = args;
|
|
454
|
+
if (!a.id)
|
|
455
|
+
throw new Error("id required");
|
|
456
|
+
let path = `/api/v1/prompts/${encodeURIComponent(a.id)}`;
|
|
457
|
+
if (a.variables) {
|
|
458
|
+
const params = new URLSearchParams();
|
|
459
|
+
for (const [k, v] of Object.entries(a.variables)) {
|
|
460
|
+
params.append(`var_${k}`, v);
|
|
461
|
+
}
|
|
462
|
+
path += `?${params.toString()}`;
|
|
463
|
+
}
|
|
464
|
+
const result = await tenpoFetch(path);
|
|
465
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
466
|
+
}
|
|
467
|
+
case "tenpo_list_prompts": {
|
|
468
|
+
const a = args;
|
|
469
|
+
const path = a.q
|
|
470
|
+
? `/api/v1/prompts?q=${encodeURIComponent(a.q)}`
|
|
471
|
+
: `/api/v1/prompts`;
|
|
472
|
+
const result = await tenpoFetch(path);
|
|
473
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
474
|
+
}
|
|
475
|
+
case "tenpo_connect_integration": {
|
|
476
|
+
const a = args;
|
|
477
|
+
if (!a.integration_id)
|
|
478
|
+
throw new Error("integration_id required");
|
|
479
|
+
if (a.credentials) {
|
|
480
|
+
const result = await tenpoFetch("/api/v1/connect/save", {
|
|
481
|
+
method: "POST",
|
|
482
|
+
body: { integration_id: a.integration_id, credentials: a.credentials },
|
|
483
|
+
});
|
|
484
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
485
|
+
}
|
|
486
|
+
const result = await tenpoFetch("/api/v1/connect/start", {
|
|
487
|
+
method: "POST",
|
|
488
|
+
body: { integration_id: a.integration_id },
|
|
489
|
+
});
|
|
490
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
491
|
+
}
|
|
492
|
+
case "tenpo_remember": {
|
|
493
|
+
const a = args;
|
|
494
|
+
const content = a.content ?? a.fact;
|
|
495
|
+
if (!content)
|
|
496
|
+
throw new Error("content required");
|
|
497
|
+
const result = await tenpoFetch("/api/v1/tools/tenpo_remember", {
|
|
498
|
+
method: "POST",
|
|
499
|
+
body: { content, scope: a.scope ?? a.memory_type ?? "fact", mode: a.mode ?? "append" },
|
|
500
|
+
});
|
|
501
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
502
|
+
}
|
|
503
|
+
// tenpo_pattern_detect, tenpo_fetch_url, tenpo_write_md, tenpo_route_skills,
|
|
504
|
+
// tenpo_overview cases DROPPED — host AI's native capabilities cover them.
|
|
505
|
+
default: {
|
|
506
|
+
// Dynamic-tool path. The 175 gateway tools + meta-tools were surfaced
|
|
507
|
+
// as first-class MCP tools by `loadDynamicTools()`. When the host AI
|
|
508
|
+
// calls one of them directly (instead of going through tenpo_run_tool),
|
|
509
|
+
// proxy it to the runtime's tool-invoke endpoint here.
|
|
510
|
+
const dynamic = await loadDynamicTools();
|
|
511
|
+
const exists = dynamic.some((t) => t.name === name);
|
|
512
|
+
if (!exists) {
|
|
513
|
+
throw new Error(`Unknown tool: ${name}. Use tenpo_route to discover available tools.`);
|
|
514
|
+
}
|
|
515
|
+
const result = await tenpoFetch(`/api/v1/tools/${encodeURIComponent(name)}`, { method: "POST", body: args });
|
|
516
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
return {
|
|
522
|
+
content: [
|
|
523
|
+
{
|
|
524
|
+
type: "text",
|
|
525
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
isError: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
// ── Resources ───────────────────────────────────────────────────────────────
|
|
533
|
+
const RESOURCES = [
|
|
534
|
+
// ════ Tenpo's brain (read on demand, not prescriptive) ════
|
|
535
|
+
{ uri: "tenpo://soul", name: "Tenpo Soul (5 always-on skills)", mimeType: "application/json", description: "Tenpo's operator personality + commerce/psychology/db skills (5 always-on). Read once per session if you want to sound like Tenpo's deep playbook; otherwise the minimal soul is in tenpo_route's response." },
|
|
536
|
+
{ uri: "tenpo://machine_rules", name: "Machine Rules (concise)", mimeType: "application/json", description: "Essential operating rules — data integrity, SQL safety, output shape, cascade pattern, memory primitive. ~100 lines, not the old 1.2K-line lecture." },
|
|
537
|
+
{ uri: "tenpo://playbook", name: "Operational Playbook", mimeType: "application/json", description: "Authored operational scenario investigations (Hidden Stockout Crisis, ad-spend bleed, payment cliff). Reference when diagnosing complex multi-domain problems." },
|
|
538
|
+
{ uri: "tenpo://routines", name: "20 Cron Routines", mimeType: "application/json", description: "20 portable routine specs (revenue dip, OOS check, ad efficiency, etc.) — clone to a merchant or run the prompt directly." },
|
|
539
|
+
// ════ PER-MERCHANT CONTEXT ════
|
|
540
|
+
{ uri: "tenpo://user_md", name: "User Profile (USER.md, MEMORY.md, CONTEXT.md, PROFILE.md + last 7 daily session memos)", mimeType: "application/json", description: "Per-merchant markdown context files PLUS last 7 days of session memos (memory/YYYY-MM-DD.md). Currency, language, tone, business model, what was discussed recently." },
|
|
541
|
+
{ uri: "tenpo://cross_session_memory", name: "Cross-Session Memory", mimeType: "application/json", description: "Full cross-session memory: facts, preferences, terminology, recent_actions. Read this BEFORE answering anything that might reference past context." },
|
|
542
|
+
{ uri: "tenpo://memory", name: "Quick Memory", mimeType: "application/json", description: "Lightweight per-merchant memory snapshot (faster than cross_session_memory)." },
|
|
543
|
+
{ uri: "tenpo://graduated_routines", name: "Graduated Routines (learned patterns)", mimeType: "application/json", description: "Routines this merchant has automated after multiple manual approvals. Tells you what they care enough about to lock in." },
|
|
544
|
+
// ════ LIVE DATA ════
|
|
545
|
+
{ uri: "tenpo://schema", name: "DB Schema", mimeType: "application/json", description: "Full DuckDB schema (all tables + columns) so the host AI can write SQL via tenpo_query." },
|
|
546
|
+
{ uri: "tenpo://integrations", name: "Connected Integrations", mimeType: "application/json", description: "Which integrations are connected for this merchant (Shopify, Klaviyo, Meta Ads, ...) and their last-sync state, plus the catalog of available integrations." },
|
|
547
|
+
{ uri: "tenpo://recent_alerts", name: "Recent Alerts (24h)", mimeType: "application/json", description: "Anything Tenpo's background heartbeat + 39-adapter sync flagged in the last 24h." },
|
|
548
|
+
{ uri: "tenpo://intelligence_snapshot", name: "Intelligence Snapshot (heartbeat data, no LLM)", mimeType: "application/json", description: "What Tenpo's background routines have captured — active alerts, recent insights, pending approvals, deterministic detector signals, integration health. Pure data, no Tenpo LLM." },
|
|
549
|
+
{ uri: "tenpo://sync_status", name: "Sync Status (39-adapter health)", mimeType: "application/json", description: "Per-integration last_sync_at, last_sync_rows, error_count, health (healthy|stale|errored|inactive). Surface this when merchant asks 'is my data current?'" },
|
|
550
|
+
{ uri: "tenpo://autonomous_incidents", name: "Autonomous Incidents (last 7d)", mimeType: "application/json", description: "Last 7 days of deterministic detector hits + tier-2 LLM scout findings. dedup-protected severity stream — what Tenpo flagged while merchant was offline." },
|
|
551
|
+
{ uri: "tenpo://action_patterns", name: "Action Patterns (graduation candidates)", mimeType: "application/json", description: "Per-pattern view of skill graduation: which actions are at 1/3, 2/3, 3/3 approvals. 3/3 auto-graduates to a recurring routine on the next hour." },
|
|
552
|
+
{ uri: "tenpo://skills_index", name: "Skills Index (categorized)", mimeType: "application/json", description: "All 200+ skills bucketed by category (psychology, strategy, marketing, market-research, ad-library-intel, video-creative, customers, inventory, finance, etc.) for fast lookup without semantic routing." },
|
|
553
|
+
{ uri: "tenpo://recommended_mcps", name: "Recommended MCPs (complementary)", mimeType: "application/json", description: "External MCPs that pair well with Tenpo (Higgsfield for video, Apify for scraping, Replicate for image/video gen, Browserbase for headless browser, Stripe official, ElevenLabs, etc.). Surface to the merchant when their need maps to one of these — install alongside Tenpo." },
|
|
554
|
+
];
|
|
555
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
556
|
+
resources: RESOURCES,
|
|
557
|
+
}));
|
|
558
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
559
|
+
await ensureApiKey();
|
|
560
|
+
const uri = request.params.uri;
|
|
561
|
+
if (!uri.startsWith("tenpo://")) {
|
|
562
|
+
throw new Error(`Unknown URI scheme: ${uri}`);
|
|
563
|
+
}
|
|
564
|
+
const resource = uri.slice("tenpo://".length);
|
|
565
|
+
const result = await tenpoFetch(`/api/v1/resources/${encodeURIComponent(resource)}`);
|
|
566
|
+
return {
|
|
567
|
+
contents: [
|
|
568
|
+
{
|
|
569
|
+
uri,
|
|
570
|
+
mimeType: "application/json",
|
|
571
|
+
text: JSON.stringify(result, null, 2),
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
// ── Prompts ─────────────────────────────────────────────────────────────────
|
|
577
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
578
|
+
await ensureApiKey();
|
|
579
|
+
try {
|
|
580
|
+
const result = (await tenpoFetch("/api/v1/prompts"));
|
|
581
|
+
const prompts = (result.prompts ?? []).map((p) => ({
|
|
582
|
+
name: p.id,
|
|
583
|
+
description: `${p.name}: ${p.description}`.slice(0, 200),
|
|
584
|
+
}));
|
|
585
|
+
return { prompts };
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
return { prompts: [] };
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
592
|
+
await ensureApiKey();
|
|
593
|
+
const id = request.params.name;
|
|
594
|
+
const variables = (request.params.arguments ?? {});
|
|
595
|
+
const params = new URLSearchParams();
|
|
596
|
+
for (const [k, v] of Object.entries(variables)) {
|
|
597
|
+
params.append(`var_${k}`, v);
|
|
598
|
+
}
|
|
599
|
+
const path = `/api/v1/prompts/${encodeURIComponent(id)}${params.toString() ? `?${params.toString()}` : ""}`;
|
|
600
|
+
const result = (await tenpoFetch(path));
|
|
601
|
+
return {
|
|
602
|
+
description: result.description ?? "",
|
|
603
|
+
messages: [
|
|
604
|
+
{
|
|
605
|
+
role: "user",
|
|
606
|
+
content: {
|
|
607
|
+
type: "text",
|
|
608
|
+
text: result.content ?? "(skill content not available)",
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
// ── Boot ────────────────────────────────────────────────────────────────────
|
|
615
|
+
async function main() {
|
|
616
|
+
log(`Starting Tenpo MCP server (api: ${TENPO_API_BASE})`);
|
|
617
|
+
const transport = new StdioServerTransport();
|
|
618
|
+
await server.connect(transport);
|
|
619
|
+
log("Connected via stdio. Ready for requests.");
|
|
620
|
+
}
|
|
621
|
+
main().catch((err) => {
|
|
622
|
+
log("Fatal error:", err);
|
|
623
|
+
process.exit(1);
|
|
624
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenpo/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Tenpo MCP — give any AI tool (Claude Code, Cursor, Claude Desktop, Codex) deterministic access to your store data, supplier scorecards, customer intelligence, creative pattern library, and 229 commerce-ops tools. Free tier, no LLM cost on our side.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://tenpo.ai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/tenpo-ai/tenpo-mcp"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/tenpo-ai/tenpo-mcp/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"tenpo-mcp": "dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"start": "node dist/index.js",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"tenpo",
|
|
35
|
+
"shopify",
|
|
36
|
+
"woocommerce",
|
|
37
|
+
"klaviyo",
|
|
38
|
+
"meta-ads",
|
|
39
|
+
"ecommerce",
|
|
40
|
+
"ai-agent",
|
|
41
|
+
"claude-code",
|
|
42
|
+
"cursor"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.0.0",
|
|
52
|
+
"tsx": "^4.0.0",
|
|
53
|
+
"typescript": "^5.5.0"
|
|
54
|
+
}
|
|
55
|
+
}
|