jujugrowth-mcp 1.2.0 → 1.4.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/package.json +1 -1
- package/server.mjs +93 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jujugrowth-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MCP server connecting your AI coding assistant (Claude, Cursor, Codex) to jujugrowth — pull your living marketing plan's dev tasks + recommendations and apply them in your repo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.mjs
CHANGED
|
@@ -37,6 +37,10 @@ async function call(method, path, body) {
|
|
|
37
37
|
// site (serverInfo + a banner on every result) so a dev-AI with several
|
|
38
38
|
// jujugrowth servers on one machine can never act on the wrong site.
|
|
39
39
|
let SITE_LABEL = null; // e.g. "jujublocks.com"
|
|
40
|
+
// #226 security (owner 2026-06-22: "only users I approve can use OR SEE the system-tools"). Operator (admin)
|
|
41
|
+
// tokens unlock the SYSTEM tools (the opportunity build-channel, the capability-gap backlog); a customer token
|
|
42
|
+
// never even SEES them (filtered from tools/list below). The control-API ALSO 403s the calls (defense in depth).
|
|
43
|
+
let IS_OPERATOR = false;
|
|
40
44
|
async function loadIdentity() {
|
|
41
45
|
try {
|
|
42
46
|
const me = await call("GET", "/whoami");
|
|
@@ -44,6 +48,11 @@ async function loadIdentity() {
|
|
|
44
48
|
} catch {
|
|
45
49
|
SITE_LABEL = null;
|
|
46
50
|
}
|
|
51
|
+
try {
|
|
52
|
+
IS_OPERATOR = (await call("GET", "/is-operator")).operator === true;
|
|
53
|
+
} catch {
|
|
54
|
+
IS_OPERATOR = false;
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
const siteBanner = () =>
|
|
49
58
|
SITE_LABEL
|
|
@@ -156,11 +165,84 @@ const TOOLS = [
|
|
|
156
165
|
},
|
|
157
166
|
{
|
|
158
167
|
name: "list_capability_gaps",
|
|
168
|
+
operator: true,
|
|
159
169
|
description:
|
|
160
170
|
"ADMIN/OPERATOR TOKEN ONLY. The jujugrowth system's OWN dev backlog: platform rules it has LEARNED (from docs + real platform responses) but cannot yet check because no collector observes the property the rule needs. Each item names the generic tool to build — `subject.attribute` per platform — with an example rule. Build that collector in the jujugrowth repo (make it write the property as a flat dotted attribute key on the synced entity); the gap auto-resolves once the fact is observable. The list is generated from real learned rules, never a hardcoded checklist. Returns 403 for non-admin tokens.",
|
|
161
171
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
162
172
|
run: async () => call("GET", "/capability-gaps"),
|
|
163
173
|
},
|
|
174
|
+
// ── #226 OPPORTUNITY BUILD-CHANNEL (OPERATOR/ADMIN TOKEN ONLY) ──────────────────────────────────────────────
|
|
175
|
+
{
|
|
176
|
+
name: "list_build_opportunities",
|
|
177
|
+
operator: true,
|
|
178
|
+
description:
|
|
179
|
+
"OPERATOR TOKEN ONLY. The owner's build worklist: APPROVED opportunities to turn into live assets (their own microSaaS, and where it fits, a partner API that also feeds jujugrowth). Returns each opportunity's id, name, summary and status. For each one, call get_opportunity_build_brief, build + DEPLOY the asset in your repo, then mark_opportunity_built. Returns 403 for non-admin tokens.",
|
|
180
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
181
|
+
run: async () => call("GET", "/build-opportunities"),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "get_build_resources",
|
|
185
|
+
operator: true,
|
|
186
|
+
description:
|
|
187
|
+
"OPERATOR TOKEN ONLY. The BUILD RESOURCE MANIFEST: how to reach every portfolio integration when building an asset — DOMAIN (Namecheap: register AND verify the domain), EMAIL (Resend), IMAGE_GEN (JUJU-PERFECT), GEO (jujuGEO), GEM (Google Merchant), METRICS (push into jujugrowth metric_facts), GA4, JG_HOOKS (GTM + monitoring beacon + partner contract), GITHUB, AWS — plus the cross-cutting requirements (design, security, compliance, backups, cost, born-safe) to bake in. SECURITY: it serves the spec + how-to + secret NAMES only, NEVER raw secret values; read each value at build time by its name from .env / Secrets Manager. Call this once before building. Returns 403 for non-admin tokens.",
|
|
188
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
189
|
+
run: async () => call("GET", "/build-resources"),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "get_opportunity_build_brief",
|
|
193
|
+
operator: true,
|
|
194
|
+
description:
|
|
195
|
+
"OPERATOR TOKEN ONLY. The FULL, self-contained build brief for ONE approved opportunity (id from list_build_opportunities): the scored dossier, the asset identity, the required JG runtime hooks, the marketing plan, the optional JG partner-API integration + a handoff prompt for jujugrowth's own dev-AI, the deploy path, and the definition of done — plus the structured dossier (per-dimension scores + evidence + promotability). Build it for real and DEPLOY it live (not localhost). Returns 403 for non-admin tokens.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: { id: { type: "string", description: "the opportunity id" } },
|
|
199
|
+
required: ["id"],
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
},
|
|
202
|
+
run: async (a) => call("GET", `/build-opportunities/${a.id}`),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "submit_opportunity_plan",
|
|
206
|
+
operator: true,
|
|
207
|
+
description:
|
|
208
|
+
"OPERATOR TOKEN ONLY. PLAN FIRST — before building, submit your plan for the owner's review: what you'll build, the stack, pages/features, design direction, integrations (domain, GA4, JG hooks, partner API), and the cost shape. Pass the opportunity `id` and the `plan` (markdown). The owner reviews it in the Opportunity Explorer; poll get_opportunity_plan_status and build only once it's approved.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
id: { type: "string", description: "the opportunity id" },
|
|
213
|
+
plan: { type: "string", description: "the build plan (markdown)" },
|
|
214
|
+
},
|
|
215
|
+
required: ["id", "plan"],
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
},
|
|
218
|
+
run: async (a) => call("POST", `/build-opportunities/${a.id}/plan`, { plan: a.plan }),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "get_opportunity_plan_status",
|
|
222
|
+
operator: true,
|
|
223
|
+
description:
|
|
224
|
+
"OPERATOR TOKEN ONLY. The owner's review decision on your submitted plan: 'submitted' (wait), 'approved' (BUILD it), or 'changes_requested' (revise per the feedback, then submit_opportunity_plan again). Pass the opportunity `id`.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: { id: { type: "string", description: "the opportunity id" } },
|
|
228
|
+
required: ["id"],
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
},
|
|
231
|
+
run: async (a) => call("GET", `/build-opportunities/${a.id}/plan-status`),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "mark_opportunity_built",
|
|
235
|
+
operator: true,
|
|
236
|
+
description:
|
|
237
|
+
"OPERATOR TOKEN ONLY. Report an opportunity BUILT after you've actually built + DEPLOYED it live (not localhost). jujugrowth marks it 'built', awaiting the owner's go-live approval; once approved, JG reconnects its tracking + promotes it. Pass the opportunity `id`. Returns 403 for non-admin tokens.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: { id: { type: "string", description: "the opportunity id" } },
|
|
241
|
+
required: ["id"],
|
|
242
|
+
additionalProperties: false,
|
|
243
|
+
},
|
|
244
|
+
run: async (a) => call("POST", `/build-opportunities/${a.id}/built`),
|
|
245
|
+
},
|
|
164
246
|
];
|
|
165
247
|
|
|
166
248
|
function send(msg) {
|
|
@@ -193,21 +275,22 @@ async function handle(msg) {
|
|
|
193
275
|
"3) Before changing anything, VERIFY current-state claims against the actual code — they can be stale; skip what's already done.\n" +
|
|
194
276
|
"4) Implement in the user's repo, scoped to wording/structure/SEO/tracking the plan asks for. Show a diff / open a PR for the user to review.\n" +
|
|
195
277
|
"5) ALWAYS report completion once shipped, don't wait to be told: mark_recommendation_handled for a recommendation, or mark_plan_task_handled (with the task's action + a note) for a plan task — that's how the plan learns the work happened and marks the step done.\n" +
|
|
196
|
-
"Campaigns and ad budgets are NOT here — those are owner decisions the jujugrowth system runs itself. This server is bound to ONE site; never act on another
|
|
278
|
+
"Campaigns and ad budgets are NOT here — those are owner decisions the jujugrowth system runs itself. This server is bound to ONE site; never act on another.\n" +
|
|
279
|
+
"\nOPERATOR (admin token only) — the BUILD CHANNEL: list_build_opportunities → get_opportunity_build_brief → get_build_resources (the integration manifest: domain, EMAIL via Resend, image-gen, GEO/GEM, JG hooks, GA4, GitHub, AWS — secret NAMES only, never values) → submit_opportunity_plan (PLAN FIRST, owner approves) → build + DEPLOY live → mark_opportunity_built. You MUST register AND VERIFY the asset's domain (DNS propagation + TLS + Resend email-auth records) — a bought-but-unverified domain is NOT done. These operator tools are hidden from non-admin tokens.",
|
|
197
280
|
},
|
|
198
281
|
});
|
|
199
282
|
}
|
|
200
283
|
if (method === "notifications/initialized") return; // no response to notifications
|
|
201
284
|
if (method === "tools/list") {
|
|
285
|
+
// #226 security: a non-operator token never even SEES the system tools (operator:true). The control-API
|
|
286
|
+
// also 403s the calls — this is the visibility half of "only users I approve can use OR SEE them".
|
|
202
287
|
return send({
|
|
203
288
|
jsonrpc: "2.0",
|
|
204
289
|
id,
|
|
205
290
|
result: {
|
|
206
|
-
tools: TOOLS.
|
|
207
|
-
name,
|
|
208
|
-
|
|
209
|
-
inputSchema,
|
|
210
|
-
})),
|
|
291
|
+
tools: TOOLS.filter((t) => IS_OPERATOR || !t.operator).map(
|
|
292
|
+
({ name, description, inputSchema }) => ({ name, description, inputSchema }),
|
|
293
|
+
),
|
|
211
294
|
},
|
|
212
295
|
});
|
|
213
296
|
}
|
|
@@ -215,6 +298,10 @@ async function handle(msg) {
|
|
|
215
298
|
const tool = TOOLS.find((t) => t.name === params?.name);
|
|
216
299
|
if (!tool)
|
|
217
300
|
return send({ jsonrpc: "2.0", id, error: { code: -32601, message: "unknown tool" } });
|
|
301
|
+
// Defense in depth: refuse an operator tool for a non-operator token even if it was somehow invoked directly
|
|
302
|
+
// (the control-API is the authoritative 403; this avoids a pointless round-trip + leaking the tool exists).
|
|
303
|
+
if (tool.operator && !IS_OPERATOR)
|
|
304
|
+
return send({ jsonrpc: "2.0", id, error: { code: -32601, message: "unknown tool" } });
|
|
218
305
|
try {
|
|
219
306
|
if (!TOKEN)
|
|
220
307
|
throw new Error("JUJUGROWTH_TOKEN not set — generate one in Settings → Developer");
|