@capivv/mcp-server 0.1.3 → 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/README.md +73 -2
- package/dist/client.d.ts +15 -3
- package/dist/client.js +45 -5
- package/dist/config.js +1 -1
- package/dist/http.d.ts +12 -0
- package/dist/http.js +102 -0
- package/dist/resources/guides.d.ts +2 -0
- package/dist/resources/guides.js +81 -0
- package/dist/resources/index.js +4 -0
- package/dist/resources/quickstart.d.ts +2 -0
- package/dist/resources/quickstart.js +173 -0
- package/dist/resources/rules.js +8 -2
- package/dist/tools/activate-rule.d.ts +3 -0
- package/dist/tools/activate-rule.js +9 -0
- package/dist/tools/api-key-usage.d.ts +3 -0
- package/dist/tools/api-key-usage.js +70 -0
- package/dist/tools/apply-rule.js +54 -13
- package/dist/tools/autopilot-status.d.ts +3 -0
- package/dist/tools/autopilot-status.js +117 -0
- package/dist/tools/create-app.d.ts +3 -0
- package/dist/tools/create-app.js +13 -0
- package/dist/tools/create-entitlement.d.ts +3 -0
- package/dist/tools/create-entitlement.js +11 -0
- package/dist/tools/create-product.d.ts +3 -0
- package/dist/tools/create-product.js +42 -0
- package/dist/tools/delete-product.d.ts +3 -0
- package/dist/tools/delete-product.js +9 -0
- package/dist/tools/delete-rule.d.ts +3 -0
- package/dist/tools/delete-rule.js +9 -0
- package/dist/tools/import-products.js +7 -5
- package/dist/tools/index.js +40 -1
- package/dist/tools/list-entitlements.d.ts +3 -0
- package/dist/tools/list-entitlements.js +6 -0
- package/dist/tools/list-rules.js +4 -2
- package/dist/tools/next-step.d.ts +3 -0
- package/dist/tools/next-step.js +123 -0
- package/dist/tools/setup-wizard.d.ts +3 -0
- package/dist/tools/setup-wizard.js +259 -0
- package/dist/tools/status.js +25 -1
- package/dist/tools/update-product.d.ts +3 -0
- package/dist/tools/update-product.js +16 -0
- package/dist/tools/verify-setup.d.ts +3 -0
- package/dist/tools/verify-setup.js +200 -0
- package/dist/tools/whoami.d.ts +3 -0
- package/dist/tools/whoami.js +31 -0
- package/dist/types.d.ts +115 -79
- package/dist/types.js +0 -2
- package/mcp.json +89 -0
- package/package.json +8 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
function buildSteps(state) {
|
|
2
|
+
const steps = [];
|
|
3
|
+
let stepNum = 1;
|
|
4
|
+
const total = 6;
|
|
5
|
+
// Step 1: App
|
|
6
|
+
steps.push({
|
|
7
|
+
step: stepNum++,
|
|
8
|
+
total_steps: total,
|
|
9
|
+
title: 'Register your app',
|
|
10
|
+
status: state.apps > 0 ? 'done' : 'needed',
|
|
11
|
+
what: 'Register your app with its platform and bundle ID.',
|
|
12
|
+
how: 'Use capivv_create_app with name, platform (ios/android/web), and bundle_id (e.g., "com.yourcompany.app").',
|
|
13
|
+
why: 'Every product and offering belongs to an app. This is the foundation of your setup.',
|
|
14
|
+
});
|
|
15
|
+
// Step 2: Entitlements
|
|
16
|
+
steps.push({
|
|
17
|
+
step: stepNum++,
|
|
18
|
+
total_steps: total,
|
|
19
|
+
title: 'Create entitlements',
|
|
20
|
+
status: state.entitlements > 0 ? 'done' : 'needed',
|
|
21
|
+
what: 'Define what features users unlock when they purchase (e.g., "pro", "premium").',
|
|
22
|
+
how: 'Use capivv_create_entitlement with identifier (e.g., "pro") and display_name (e.g., "Pro Access").',
|
|
23
|
+
why: 'Entitlements are how your app checks "does this user have access?" They decouple features from specific products.',
|
|
24
|
+
});
|
|
25
|
+
// Step 3: Store credentials (can't be done via MCP)
|
|
26
|
+
const storeStep = state.nextOnboardingStep === 'store_credentials_configured' || state.nextOnboardingStep === 'store_connected'
|
|
27
|
+
? 'needed'
|
|
28
|
+
: state.products > 0
|
|
29
|
+
? 'done'
|
|
30
|
+
: 'optional';
|
|
31
|
+
steps.push({
|
|
32
|
+
step: stepNum++,
|
|
33
|
+
total_steps: total,
|
|
34
|
+
title: 'Connect store credentials',
|
|
35
|
+
status: storeStep,
|
|
36
|
+
what: 'Connect your App Store Connect / Google Play credentials so Capivv can validate receipts and import products.',
|
|
37
|
+
how: 'Go to the Capivv dashboard → Settings → Developer → Integrations. For iOS: upload your App Store Connect API key (p8 file). For Android: upload your Google Play service account JSON.',
|
|
38
|
+
why: 'Without store credentials, Capivv cannot verify purchases or automatically sync subscription status changes.',
|
|
39
|
+
});
|
|
40
|
+
// Step 4: Products
|
|
41
|
+
steps.push({
|
|
42
|
+
step: stepNum++,
|
|
43
|
+
total_steps: total,
|
|
44
|
+
title: 'Create products',
|
|
45
|
+
status: state.products > 0 ? 'done' : 'needed',
|
|
46
|
+
what: 'Create your subscription or in-app purchase products, linked to entitlements.',
|
|
47
|
+
how: state.apps > 0
|
|
48
|
+
? 'Use capivv_create_product with app_id, external_id (must match your store product ID exactly), product_type, and entitlement_ids. Or use capivv_import_products to auto-import from a connected store.'
|
|
49
|
+
: 'First complete Step 1 (create an app), then use capivv_create_product.',
|
|
50
|
+
why: 'Products map to real App Store / Google Play items. The external_id must match exactly or purchase verification will fail.',
|
|
51
|
+
});
|
|
52
|
+
// Step 5: Offerings
|
|
53
|
+
steps.push({
|
|
54
|
+
step: stepNum++,
|
|
55
|
+
total_steps: total,
|
|
56
|
+
title: 'Create an offering',
|
|
57
|
+
status: state.offerings > 0 ? 'done' : 'needed',
|
|
58
|
+
what: 'Bundle your products into an offering that your paywall will display.',
|
|
59
|
+
how: state.products > 0
|
|
60
|
+
? 'Use capivv_create_offering with your app_id, an identifier (e.g., "default"), and packages that reference your product IDs.'
|
|
61
|
+
: 'First complete Step 4 (create products), then use capivv_create_offering.',
|
|
62
|
+
why: 'The SDK fetches offerings to populate your paywall. Without an offering, your app has nothing to show users.',
|
|
63
|
+
});
|
|
64
|
+
// Step 6: SDK integration
|
|
65
|
+
steps.push({
|
|
66
|
+
step: stepNum++,
|
|
67
|
+
total_steps: total,
|
|
68
|
+
title: 'Integrate the SDK',
|
|
69
|
+
status: state.onboardingComplete ? 'done' : 'needed',
|
|
70
|
+
what: 'Add the Capivv SDK to your app, configure it with your public API key, and implement the paywall.',
|
|
71
|
+
how: 'Get your public key from the Capivv dashboard (Settings → Developer → API Keys). Read capivv://docs/quickstart for code examples, or capivv://docs/guides/ios for a detailed iOS walkthrough.',
|
|
72
|
+
why: 'The SDK handles user identification, entitlement checking, purchase processing, and receipt validation.',
|
|
73
|
+
});
|
|
74
|
+
return steps;
|
|
75
|
+
}
|
|
76
|
+
export function registerNextStepTool(server, client) {
|
|
77
|
+
server.tool('capivv_next_step', 'Analyze your current Capivv setup and tell you exactly what to do next. Returns a prioritized checklist with clear instructions for each step.', async () => {
|
|
78
|
+
const [apps, products, entitlements, offerings, rules, onboarding] = await Promise.allSettled([
|
|
79
|
+
client.listApps(),
|
|
80
|
+
client.listProducts(),
|
|
81
|
+
client.listEntitlements(),
|
|
82
|
+
client.listOfferings(),
|
|
83
|
+
client.listRules(),
|
|
84
|
+
client.getOnboarding(),
|
|
85
|
+
]);
|
|
86
|
+
const count = (r) => r.status === 'fulfilled' ? r.value.length : 0;
|
|
87
|
+
const state = {
|
|
88
|
+
apps: count(apps),
|
|
89
|
+
entitlements: count(entitlements),
|
|
90
|
+
products: count(products),
|
|
91
|
+
offerings: count(offerings),
|
|
92
|
+
rules: count(rules),
|
|
93
|
+
onboardingComplete: onboarding.status === 'fulfilled' && onboarding.value.is_complete,
|
|
94
|
+
nextOnboardingStep: onboarding.status === 'fulfilled' ? onboarding.value.next_step : null,
|
|
95
|
+
};
|
|
96
|
+
const steps = buildSteps(state);
|
|
97
|
+
const completed = steps.filter((s) => s.status === 'done').length;
|
|
98
|
+
const needed = steps.filter((s) => s.status === 'needed');
|
|
99
|
+
const nextStep = needed[0] ?? null;
|
|
100
|
+
const result = {
|
|
101
|
+
progress: `${completed}/${steps.length} steps completed`,
|
|
102
|
+
setup_complete: needed.length === 0,
|
|
103
|
+
steps,
|
|
104
|
+
};
|
|
105
|
+
if (nextStep) {
|
|
106
|
+
result.next_action = {
|
|
107
|
+
summary: `Step ${nextStep.step}/${nextStep.total_steps}: ${nextStep.title}`,
|
|
108
|
+
what: nextStep.what,
|
|
109
|
+
how: nextStep.how,
|
|
110
|
+
why: nextStep.why,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
result.next_action = {
|
|
115
|
+
summary: 'Setup complete! Your Capivv integration is ready.',
|
|
116
|
+
what: 'Your app, entitlements, products, and offerings are all configured.',
|
|
117
|
+
how: 'Use capivv_get_analytics to monitor your subscription metrics, or capivv_apply_rule to add business rules like trial offers or churn recovery.',
|
|
118
|
+
why: 'You can now focus on optimizing conversions and reducing churn.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Only the messages we're confident indicate a conflict (existing row) should
|
|
4
|
+
* trigger the "list and reuse" fallback. Anything else — timeouts, 5xx,
|
|
5
|
+
* validation errors — is a legitimate failure that the caller needs to see.
|
|
6
|
+
*/
|
|
7
|
+
function isConflictError(msg) {
|
|
8
|
+
const lower = msg.toLowerCase();
|
|
9
|
+
return (lower.includes('already exists') ||
|
|
10
|
+
lower.includes('duplicate') ||
|
|
11
|
+
lower.includes('409') ||
|
|
12
|
+
lower.includes('conflict'));
|
|
13
|
+
}
|
|
14
|
+
export function registerSetupWizardTool(server, client) {
|
|
15
|
+
server.tool('capivv_setup_wizard', `One-shot setup for an EXISTING app: creates an entitlement, products, and offering on an app you've already imported via the Capivv dashboard's store-connect flow.
|
|
16
|
+
|
|
17
|
+
As of V6 this tool does NOT create apps from scratch — connect your store in the dashboard to import apps with real bundle IDs. Use capivv_list_apps or capivv_whoami to see which apps are available.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
bundle_id: "com.mycompany.myapp" // must already exist; omit if your tenant has exactly one app
|
|
21
|
+
entitlement_id: "pro"
|
|
22
|
+
products: [
|
|
23
|
+
{ name: "Monthly", external_id: "com.mycompany.myapp.monthly", billing_period: "month", price_cents: 799 },
|
|
24
|
+
{ name: "Annual", external_id: "com.mycompany.myapp.yearly", billing_period: "year", price_cents: 4999 }
|
|
25
|
+
]
|
|
26
|
+
offering_id: "default"`, {
|
|
27
|
+
bundle_id: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Bundle ID of the existing app to configure. If omitted and the tenant has exactly one app, that app is used.'),
|
|
31
|
+
entitlement_id: z.string().describe('Entitlement identifier (e.g., "pro", "premium")'),
|
|
32
|
+
entitlement_name: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Entitlement display name (defaults to capitalized identifier)'),
|
|
36
|
+
products: z
|
|
37
|
+
.array(z.object({
|
|
38
|
+
name: z.string().describe('Product display name (e.g., "Monthly Plan")'),
|
|
39
|
+
external_id: z
|
|
40
|
+
.string()
|
|
41
|
+
.describe('Store product ID — must match App Store Connect / Google Play exactly'),
|
|
42
|
+
billing_period: z
|
|
43
|
+
.string()
|
|
44
|
+
.describe('Billing period: week, month, three_months, six_months, or year'),
|
|
45
|
+
price_cents: z.number().describe('Price in USD cents (e.g., 799 for $7.99)'),
|
|
46
|
+
}))
|
|
47
|
+
.describe('Subscription products to create'),
|
|
48
|
+
offering_id: z.string().optional().describe('Offering identifier (default: "default")'),
|
|
49
|
+
offering_name: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Offering display name (default: based on entitlement)'),
|
|
53
|
+
}, async (args) => {
|
|
54
|
+
// Step 0: resolve identity + reachable apps. Every wizard response
|
|
55
|
+
// carries `tenant` so the caller can see which workspace is being
|
|
56
|
+
// written to — if this org_name doesn't match expectations, stop.
|
|
57
|
+
const me = await client.whoami();
|
|
58
|
+
const result = {
|
|
59
|
+
success: true,
|
|
60
|
+
tenant: { tenant_id: me.tenant_id, org_name: me.org_name },
|
|
61
|
+
resolved_app: null,
|
|
62
|
+
created: { entitlements: [], products: [] },
|
|
63
|
+
skipped_existing: [],
|
|
64
|
+
errors: [],
|
|
65
|
+
next_steps: [],
|
|
66
|
+
};
|
|
67
|
+
// Step 1: resolve which app to configure. No more hardcoded placeholder
|
|
68
|
+
// app creation — the app must already exist via dashboard import.
|
|
69
|
+
let selected = null;
|
|
70
|
+
if (me.apps.length === 0) {
|
|
71
|
+
result.success = false;
|
|
72
|
+
result.errors.push(`Workspace "${me.org_name}" has no apps yet. Connect your store in the dashboard (Settings → Developer → Integrations) and import at least one app first, then re-run this wizard.`);
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (args.bundle_id) {
|
|
78
|
+
const match = me.apps.find((a) => a.bundle_id === args.bundle_id);
|
|
79
|
+
if (!match) {
|
|
80
|
+
result.success = false;
|
|
81
|
+
result.errors.push(`No app with bundle_id "${args.bundle_id}" in workspace "${me.org_name}". Available: ${me.apps.map((a) => a.bundle_id).join(', ')}.`);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// whoami returns a subset of App fields — fetch the full row so we
|
|
87
|
+
// can use it as the appId source.
|
|
88
|
+
const apps = await client.listApps();
|
|
89
|
+
selected = apps.find((a) => a.id === match.id) ?? null;
|
|
90
|
+
}
|
|
91
|
+
else if (me.apps.length === 1) {
|
|
92
|
+
const apps = await client.listApps();
|
|
93
|
+
selected = apps.find((a) => a.id === me.apps[0].id) ?? null;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
result.success = false;
|
|
97
|
+
result.errors.push(`Workspace "${me.org_name}" has ${me.apps.length} apps — pass bundle_id to pick one. Options: ${me.apps.map((a) => `${a.bundle_id} (${a.name})`).join(', ')}.`);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!selected) {
|
|
103
|
+
result.success = false;
|
|
104
|
+
result.errors.push('Failed to resolve app from the tenant');
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
result.resolved_app = {
|
|
110
|
+
id: selected.id,
|
|
111
|
+
name: selected.name,
|
|
112
|
+
bundle_id: selected.bundle_id ?? '',
|
|
113
|
+
platform: selected.platform,
|
|
114
|
+
};
|
|
115
|
+
const appId = selected.id;
|
|
116
|
+
// Step 2: Create entitlement. Conflict (already exists) is treated as a
|
|
117
|
+
// skip, not a failure; everything else aborts loud.
|
|
118
|
+
let entitlementId;
|
|
119
|
+
try {
|
|
120
|
+
const entitlement = await client.createEntitlement({
|
|
121
|
+
identifier: args.entitlement_id,
|
|
122
|
+
display_name: args.entitlement_name ??
|
|
123
|
+
args.entitlement_id.charAt(0).toUpperCase() + args.entitlement_id.slice(1),
|
|
124
|
+
});
|
|
125
|
+
result.created.entitlements.push(entitlement);
|
|
126
|
+
entitlementId = entitlement.id;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
if (isConflictError(msg)) {
|
|
131
|
+
const entitlements = await client.listEntitlements();
|
|
132
|
+
const existing = entitlements.find((e) => e.identifier === args.entitlement_id);
|
|
133
|
+
if (existing) {
|
|
134
|
+
entitlementId = existing.id;
|
|
135
|
+
result.skipped_existing.push(`entitlement:${args.entitlement_id}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
result.success = false;
|
|
139
|
+
result.errors.push(`Entitlement "${args.entitlement_id}" reported as existing but couldn't be found — aborting`);
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
result.success = false;
|
|
147
|
+
result.errors.push(`Entitlement creation failed: ${msg}`);
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Step 3: Create products
|
|
154
|
+
const PERIOD_TO_PACKAGE = {
|
|
155
|
+
week: 'weekly',
|
|
156
|
+
month: 'monthly',
|
|
157
|
+
three_months: 'three_month',
|
|
158
|
+
six_months: 'six_month',
|
|
159
|
+
year: 'annual',
|
|
160
|
+
};
|
|
161
|
+
for (const p of args.products) {
|
|
162
|
+
try {
|
|
163
|
+
const product = await client.createProduct({
|
|
164
|
+
app_id: appId,
|
|
165
|
+
external_id: p.external_id,
|
|
166
|
+
product_type: 'subscription',
|
|
167
|
+
display_name: p.name,
|
|
168
|
+
entitlement_ids: entitlementId ? [entitlementId] : [],
|
|
169
|
+
subscription: { billing_period: p.billing_period },
|
|
170
|
+
prices: [{ currency: 'USD', amount_cents: p.price_cents, is_default: true }],
|
|
171
|
+
});
|
|
172
|
+
result.created.products.push(product);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
if (isConflictError(msg)) {
|
|
177
|
+
const products = await client.listProducts(appId);
|
|
178
|
+
const existing = products.find((pr) => pr.external_id === p.external_id);
|
|
179
|
+
if (existing) {
|
|
180
|
+
result.created.products.push(existing);
|
|
181
|
+
result.skipped_existing.push(`product:${p.external_id}`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
result.success = false;
|
|
185
|
+
result.errors.push(`Product "${p.name}" reported as existing but couldn't be found — aborting`);
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
result.success = false;
|
|
193
|
+
result.errors.push(`Product "${p.name}" creation failed: ${msg}`);
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Step 4: Create offering
|
|
201
|
+
if (result.created.products.length > 0) {
|
|
202
|
+
const offeringId = args.offering_id ?? 'default';
|
|
203
|
+
const offeringName = args.offering_name ??
|
|
204
|
+
`${args.entitlement_id.charAt(0).toUpperCase() + args.entitlement_id.slice(1)} Plans`;
|
|
205
|
+
const packages = result.created.products.map((product, index) => {
|
|
206
|
+
const inputProduct = args.products.find((p) => p.external_id === product.external_id);
|
|
207
|
+
const packageType = inputProduct
|
|
208
|
+
? (PERIOD_TO_PACKAGE[inputProduct.billing_period] ?? 'custom')
|
|
209
|
+
: 'custom';
|
|
210
|
+
return {
|
|
211
|
+
identifier: packageType,
|
|
212
|
+
product_id: product.id,
|
|
213
|
+
display_name: product.display_name,
|
|
214
|
+
package_type: packageType,
|
|
215
|
+
position: index,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
result.created.offering = await client.createOffering({
|
|
220
|
+
app_id: appId,
|
|
221
|
+
identifier: offeringId,
|
|
222
|
+
display_name: offeringName,
|
|
223
|
+
is_default: true,
|
|
224
|
+
packages,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
if (isConflictError(msg)) {
|
|
230
|
+
const offerings = await client.listOfferings();
|
|
231
|
+
const existing = offerings.find((o) => o.identifier === offeringId);
|
|
232
|
+
if (existing) {
|
|
233
|
+
result.created.offering = existing;
|
|
234
|
+
result.skipped_existing.push(`offering:${offeringId}`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
result.success = false;
|
|
238
|
+
result.errors.push(`Offering "${offeringId}" reported as existing but couldn't be found — aborting`);
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
result.success = false;
|
|
246
|
+
result.errors.push(`Offering creation failed: ${msg}`);
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Build next steps
|
|
254
|
+
result.next_steps.push(`Wrote to workspace "${result.tenant.org_name}" (tenant_id: ${result.tenant.tenant_id}, app: ${result.resolved_app.bundle_id}). If that's the wrong workspace, switch your CAPIVV_API_KEY env var before continuing.`);
|
|
255
|
+
result.next_steps.push('Integrate the SDK: copy the publishable (pk_*) key generated for this app from Settings → Developer → API Keys. Read capivv://docs/quickstart for code examples.');
|
|
256
|
+
result.next_steps.push('Verify: Run capivv_verify_setup to check that everything is configured correctly.');
|
|
257
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
258
|
+
});
|
|
259
|
+
}
|
package/dist/tools/status.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiError } from '../client.js';
|
|
1
2
|
const STEP_GUIDANCE = {
|
|
2
3
|
app_created: 'Create your first app with capivv_list_apps to see current apps, or create one in the Capivv dashboard.',
|
|
3
4
|
entitlement_created: 'Define entitlements to control feature access. Entitlements are granted when users purchase products.',
|
|
@@ -11,7 +12,7 @@ const STEP_GUIDANCE = {
|
|
|
11
12
|
};
|
|
12
13
|
export function registerStatusTool(server, client) {
|
|
13
14
|
server.tool('capivv_status', 'Get the current status of your Capivv subscription platform including setup progress, resource counts, and key metrics.', async () => {
|
|
14
|
-
const
|
|
15
|
+
const settled = await Promise.allSettled([
|
|
15
16
|
client.listApps(),
|
|
16
17
|
client.listProducts(),
|
|
17
18
|
client.listEntitlements(),
|
|
@@ -20,6 +21,26 @@ export function registerStatusTool(server, client) {
|
|
|
20
21
|
client.getOnboarding(),
|
|
21
22
|
client.getAnalyticsOverview(),
|
|
22
23
|
]);
|
|
24
|
+
const [apps, products, entitlements, offerings, rules, onboarding, analytics] = settled;
|
|
25
|
+
// Every call goes through the same Bearer-token auth. If even one returns
|
|
26
|
+
// 401, the bearer is bad and every count we'd report would be a lie —
|
|
27
|
+
// short-circuit with a clear message instead of rendering zeros.
|
|
28
|
+
const authFailure = settled.find((r) => r.status === 'rejected' && r.reason instanceof ApiError && r.reason.status === 401);
|
|
29
|
+
if (authFailure) {
|
|
30
|
+
return {
|
|
31
|
+
isError: true,
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: 'Authentication failed (HTTP 401). Verify CAPIVV_API_KEY (or the Bearer token your MCP client is sending) is a valid sk_* secret key.',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const labels = ['apps', 'products', 'entitlements', 'offerings', 'rules', 'onboarding', 'analytics'];
|
|
41
|
+
const warnings = settled
|
|
42
|
+
.map((r, i) => (r.status === 'rejected' ? `${labels[i]}: ${r.reason.message}` : null))
|
|
43
|
+
.filter((w) => w !== null);
|
|
23
44
|
const count = (r) => r.status === 'fulfilled' ? r.value.length : 0;
|
|
24
45
|
const result = {
|
|
25
46
|
resources: {
|
|
@@ -30,6 +51,9 @@ export function registerStatusTool(server, client) {
|
|
|
30
51
|
rules: count(rules),
|
|
31
52
|
},
|
|
32
53
|
};
|
|
54
|
+
if (warnings.length) {
|
|
55
|
+
result._warnings = warnings;
|
|
56
|
+
}
|
|
33
57
|
if (onboarding.status === 'fulfilled') {
|
|
34
58
|
const ob = onboarding.value;
|
|
35
59
|
result.onboarding = {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerUpdateProductTool(server, client) {
|
|
3
|
+
server.tool('capivv_update_product', 'Update an existing product. Only provided fields are changed.', {
|
|
4
|
+
product_id: z.string().describe('Product ID to update'),
|
|
5
|
+
external_id: z.string().optional().describe('New store product ID'),
|
|
6
|
+
display_name: z.string().optional().describe('New display name'),
|
|
7
|
+
description: z.string().optional().describe('New description'),
|
|
8
|
+
entitlement_ids: z
|
|
9
|
+
.array(z.string())
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('New entitlement IDs (replaces existing)'),
|
|
12
|
+
}, async ({ product_id, ...updates }) => {
|
|
13
|
+
const product = await client.updateProduct(product_id, updates);
|
|
14
|
+
return { content: [{ type: 'text', text: JSON.stringify(product, null, 2) }] };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export function registerVerifySetupTool(server, client) {
|
|
2
|
+
server.tool('capivv_verify_setup', 'Verify your Capivv setup is complete and correctly configured. Checks apps, entitlements, products, offerings, and store connectivity. Returns a pass/fail checklist with fix instructions for any issues.', async () => {
|
|
3
|
+
const [apps, products, entitlements, offerings, onboarding, health] = await Promise.allSettled([
|
|
4
|
+
client.listApps(),
|
|
5
|
+
client.listProducts(),
|
|
6
|
+
client.listEntitlements(),
|
|
7
|
+
client.listOfferings(),
|
|
8
|
+
client.getOnboarding(),
|
|
9
|
+
client.healthCheck(),
|
|
10
|
+
]);
|
|
11
|
+
const checks = [];
|
|
12
|
+
// 1. API connectivity
|
|
13
|
+
if (health.status === 'fulfilled' && health.value) {
|
|
14
|
+
checks.push({
|
|
15
|
+
name: 'API connectivity',
|
|
16
|
+
status: 'pass',
|
|
17
|
+
detail: 'Successfully connected to Capivv API.',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
checks.push({
|
|
22
|
+
name: 'API connectivity',
|
|
23
|
+
status: 'fail',
|
|
24
|
+
detail: 'Cannot reach Capivv API.',
|
|
25
|
+
fix: 'Check your CAPIVV_API_KEY and CAPIVV_API_URL environment variables. Ensure the API key is valid and not revoked.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// 2. Apps
|
|
29
|
+
const appList = apps.status === 'fulfilled' ? apps.value : [];
|
|
30
|
+
if (appList.length > 0) {
|
|
31
|
+
const platforms = [...new Set(appList.map((a) => a.platform))].join(', ');
|
|
32
|
+
checks.push({
|
|
33
|
+
name: 'Apps registered',
|
|
34
|
+
status: 'pass',
|
|
35
|
+
detail: `${appList.length} app(s) registered (${platforms}).`,
|
|
36
|
+
});
|
|
37
|
+
// Check for missing bundle IDs
|
|
38
|
+
const missingBundleId = appList.filter((a) => !a.bundle_id);
|
|
39
|
+
if (missingBundleId.length > 0) {
|
|
40
|
+
checks.push({
|
|
41
|
+
name: 'App bundle IDs',
|
|
42
|
+
status: 'warn',
|
|
43
|
+
detail: `${missingBundleId.length} app(s) missing bundle ID: ${missingBundleId.map((a) => a.name).join(', ')}.`,
|
|
44
|
+
fix: 'Each app should have a bundle_id matching your store listing (e.g., com.yourcompany.yourapp).',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
checks.push({
|
|
50
|
+
name: 'Apps registered',
|
|
51
|
+
status: 'fail',
|
|
52
|
+
detail: 'No apps registered.',
|
|
53
|
+
fix: 'Use capivv_create_app or capivv_setup_wizard to register your app with its platform and bundle ID.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// 3. Entitlements
|
|
57
|
+
const entitlementList = entitlements.status === 'fulfilled' ? entitlements.value : [];
|
|
58
|
+
if (entitlementList.length > 0) {
|
|
59
|
+
const ids = entitlementList.map((e) => e.identifier).join(', ');
|
|
60
|
+
checks.push({
|
|
61
|
+
name: 'Entitlements defined',
|
|
62
|
+
status: 'pass',
|
|
63
|
+
detail: `${entitlementList.length} entitlement(s): ${ids}.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
checks.push({
|
|
68
|
+
name: 'Entitlements defined',
|
|
69
|
+
status: 'fail',
|
|
70
|
+
detail: 'No entitlements defined.',
|
|
71
|
+
fix: 'Use capivv_create_entitlement to define what features users unlock (e.g., identifier: "pro", display_name: "Pro Access").',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// 4. Products
|
|
75
|
+
const productList = products.status === 'fulfilled' ? products.value : [];
|
|
76
|
+
if (productList.length > 0) {
|
|
77
|
+
checks.push({
|
|
78
|
+
name: 'Products created',
|
|
79
|
+
status: 'pass',
|
|
80
|
+
detail: `${productList.length} product(s) created.`,
|
|
81
|
+
});
|
|
82
|
+
// Check products linked to entitlements
|
|
83
|
+
const unlinked = productList.filter((p) => !p.entitlement_ids || p.entitlement_ids.length === 0);
|
|
84
|
+
if (unlinked.length > 0) {
|
|
85
|
+
checks.push({
|
|
86
|
+
name: 'Products linked to entitlements',
|
|
87
|
+
status: 'warn',
|
|
88
|
+
detail: `${unlinked.length} product(s) not linked to any entitlement: ${unlinked.map((p) => p.display_name).join(', ')}.`,
|
|
89
|
+
fix: 'Use capivv_update_product to add entitlement_ids to these products. Without entitlement links, purchases won\'t grant feature access.',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'Products linked to entitlements',
|
|
95
|
+
status: 'pass',
|
|
96
|
+
detail: 'All products are linked to at least one entitlement.',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
checks.push({
|
|
102
|
+
name: 'Products created',
|
|
103
|
+
status: 'fail',
|
|
104
|
+
detail: 'No products created.',
|
|
105
|
+
fix: 'Use capivv_create_product to create subscription products. The external_id must exactly match your App Store / Google Play product ID.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// 5. Offerings
|
|
109
|
+
const offeringList = offerings.status === 'fulfilled' ? offerings.value : [];
|
|
110
|
+
if (offeringList.length > 0) {
|
|
111
|
+
const defaultOffering = offeringList.find((o) => o.is_default);
|
|
112
|
+
const totalPackages = offeringList.reduce((sum, o) => sum + (o.packages?.length ?? 0), 0);
|
|
113
|
+
checks.push({
|
|
114
|
+
name: 'Offerings configured',
|
|
115
|
+
status: 'pass',
|
|
116
|
+
detail: `${offeringList.length} offering(s) with ${totalPackages} total package(s).`,
|
|
117
|
+
});
|
|
118
|
+
if (!defaultOffering) {
|
|
119
|
+
checks.push({
|
|
120
|
+
name: 'Default offering set',
|
|
121
|
+
status: 'warn',
|
|
122
|
+
detail: 'No offering is marked as default.',
|
|
123
|
+
fix: 'Create or update an offering with is_default: true. The SDK fetches the default offering for paywalls.',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
checks.push({
|
|
128
|
+
name: 'Default offering set',
|
|
129
|
+
status: 'pass',
|
|
130
|
+
detail: `Default offering: "${defaultOffering.identifier}" with ${defaultOffering.packages?.length ?? 0} package(s).`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Check for empty offerings
|
|
134
|
+
const emptyOfferings = offeringList.filter((o) => !o.packages || o.packages.length === 0);
|
|
135
|
+
if (emptyOfferings.length > 0) {
|
|
136
|
+
checks.push({
|
|
137
|
+
name: 'Offerings have packages',
|
|
138
|
+
status: 'warn',
|
|
139
|
+
detail: `${emptyOfferings.length} offering(s) have no packages: ${emptyOfferings.map((o) => o.identifier).join(', ')}.`,
|
|
140
|
+
fix: 'An offering without packages has nothing to display on your paywall. Add products as packages to these offerings.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
checks.push({
|
|
146
|
+
name: 'Offerings configured',
|
|
147
|
+
status: 'fail',
|
|
148
|
+
detail: 'No offerings created.',
|
|
149
|
+
fix: 'Use capivv_create_offering to bundle your products into an offering. This is what your paywall displays to users.',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// 6. Store credentials (check via onboarding)
|
|
153
|
+
if (onboarding.status === 'fulfilled') {
|
|
154
|
+
const ob = onboarding.value;
|
|
155
|
+
const storeStep = ob.steps?.find((s) => s.key === 'store_credentials_configured' || s.key === 'store_connected');
|
|
156
|
+
if (storeStep) {
|
|
157
|
+
if (storeStep.completed) {
|
|
158
|
+
checks.push({
|
|
159
|
+
name: 'Store credentials connected',
|
|
160
|
+
status: 'pass',
|
|
161
|
+
detail: 'App store credentials are configured.',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
checks.push({
|
|
166
|
+
name: 'Store credentials connected',
|
|
167
|
+
status: 'warn',
|
|
168
|
+
detail: 'Store credentials not yet configured.',
|
|
169
|
+
fix: 'Go to the Capivv dashboard → Settings → Developer → Integrations. Upload your App Store Connect API key (.p8 file) or Google Play service account JSON. This enables receipt validation and subscription status sync.',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Summary
|
|
175
|
+
const passed = checks.filter((c) => c.status === 'pass').length;
|
|
176
|
+
const failed = checks.filter((c) => c.status === 'fail').length;
|
|
177
|
+
const warned = checks.filter((c) => c.status === 'warn').length;
|
|
178
|
+
let overallStatus;
|
|
179
|
+
let summary;
|
|
180
|
+
if (failed === 0 && warned === 0) {
|
|
181
|
+
overallStatus = 'ready';
|
|
182
|
+
summary = 'Your Capivv setup is complete and ready for integration!';
|
|
183
|
+
}
|
|
184
|
+
else if (failed === 0) {
|
|
185
|
+
overallStatus = 'mostly_ready';
|
|
186
|
+
summary = `Setup is functional but has ${warned} warning(s) to address for a production-quality integration.`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
overallStatus = 'incomplete';
|
|
190
|
+
summary = `Setup is incomplete: ${failed} required item(s) missing. Fix the failing checks to get started.`;
|
|
191
|
+
}
|
|
192
|
+
const result = {
|
|
193
|
+
status: overallStatus,
|
|
194
|
+
summary,
|
|
195
|
+
score: `${passed}/${checks.length} checks passed`,
|
|
196
|
+
checks,
|
|
197
|
+
};
|
|
198
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
199
|
+
});
|
|
200
|
+
}
|