jujugrowth-mcp 1.1.0 → 1.3.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 +76 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jujugrowth-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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
|
|
@@ -88,6 +97,27 @@ const TOOLS = [
|
|
|
88
97
|
run: async (a) =>
|
|
89
98
|
call("GET", `/plan-tasks${a.tenantId ? `?tenantId=${encodeURIComponent(a.tenantId)}` : ""}`),
|
|
90
99
|
},
|
|
100
|
+
{
|
|
101
|
+
name: "mark_plan_task_handled",
|
|
102
|
+
description:
|
|
103
|
+
"Report a marketing-plan dev-AI task DONE after you've shipped it in the repo. Pass the task's `action` (exactly as returned by list_plan_tasks) and a short `note` of what you changed. jujugrowth records it and the next plan regeneration marks that step done (and stops listing it). ALWAYS call this once a plan task is implemented — it's how the plan knows the work happened. Pass `tenantId` if your token spans more than one site.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
tenantId: { type: "string", description: "the site (required only for multi-site tokens)" },
|
|
108
|
+
action: { type: "string", description: "the task's action text, exactly as listed" },
|
|
109
|
+
note: { type: "string", description: "1-3 sentences: what you implemented" },
|
|
110
|
+
},
|
|
111
|
+
required: ["action"],
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
},
|
|
114
|
+
run: async (a) =>
|
|
115
|
+
call("POST", "/plan-tasks/handled", {
|
|
116
|
+
action: a.action,
|
|
117
|
+
...(a.tenantId ? { tenantId: a.tenantId } : {}),
|
|
118
|
+
...(a.note ? { note: a.note } : {}),
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
91
121
|
{
|
|
92
122
|
name: "get_recommendation",
|
|
93
123
|
description:
|
|
@@ -135,11 +165,47 @@ const TOOLS = [
|
|
|
135
165
|
},
|
|
136
166
|
{
|
|
137
167
|
name: "list_capability_gaps",
|
|
168
|
+
operator: true,
|
|
138
169
|
description:
|
|
139
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.",
|
|
140
171
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
141
172
|
run: async () => call("GET", "/capability-gaps"),
|
|
142
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_opportunity_build_brief",
|
|
185
|
+
operator: true,
|
|
186
|
+
description:
|
|
187
|
+
"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.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: { id: { type: "string", description: "the opportunity id" } },
|
|
191
|
+
required: ["id"],
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
},
|
|
194
|
+
run: async (a) => call("GET", `/build-opportunities/${a.id}`),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "mark_opportunity_built",
|
|
198
|
+
operator: true,
|
|
199
|
+
description:
|
|
200
|
+
"OPERATOR TOKEN ONLY. Report an opportunity BUILT & live after you've deployed it. jujugrowth marks it launched, reconnects its tracking, and starts measuring its real outcome (and promoting it). Pass the opportunity `id`. Returns 403 for non-admin tokens.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: { id: { type: "string", description: "the opportunity id" } },
|
|
204
|
+
required: ["id"],
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
},
|
|
207
|
+
run: async (a) => call("POST", `/build-opportunities/${a.id}/built`),
|
|
208
|
+
},
|
|
143
209
|
];
|
|
144
210
|
|
|
145
211
|
function send(msg) {
|
|
@@ -171,22 +237,22 @@ async function handle(msg) {
|
|
|
171
237
|
"2) get_recommendation for any rec you'll act on.\n" +
|
|
172
238
|
"3) Before changing anything, VERIFY current-state claims against the actual code — they can be stale; skip what's already done.\n" +
|
|
173
239
|
"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" +
|
|
174
|
-
"5) ALWAYS
|
|
240
|
+
"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" +
|
|
175
241
|
"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.",
|
|
176
242
|
},
|
|
177
243
|
});
|
|
178
244
|
}
|
|
179
245
|
if (method === "notifications/initialized") return; // no response to notifications
|
|
180
246
|
if (method === "tools/list") {
|
|
247
|
+
// #226 security: a non-operator token never even SEES the system tools (operator:true). The control-API
|
|
248
|
+
// also 403s the calls — this is the visibility half of "only users I approve can use OR SEE them".
|
|
181
249
|
return send({
|
|
182
250
|
jsonrpc: "2.0",
|
|
183
251
|
id,
|
|
184
252
|
result: {
|
|
185
|
-
tools: TOOLS.
|
|
186
|
-
name,
|
|
187
|
-
|
|
188
|
-
inputSchema,
|
|
189
|
-
})),
|
|
253
|
+
tools: TOOLS.filter((t) => IS_OPERATOR || !t.operator).map(
|
|
254
|
+
({ name, description, inputSchema }) => ({ name, description, inputSchema }),
|
|
255
|
+
),
|
|
190
256
|
},
|
|
191
257
|
});
|
|
192
258
|
}
|
|
@@ -194,6 +260,10 @@ async function handle(msg) {
|
|
|
194
260
|
const tool = TOOLS.find((t) => t.name === params?.name);
|
|
195
261
|
if (!tool)
|
|
196
262
|
return send({ jsonrpc: "2.0", id, error: { code: -32601, message: "unknown tool" } });
|
|
263
|
+
// Defense in depth: refuse an operator tool for a non-operator token even if it was somehow invoked directly
|
|
264
|
+
// (the control-API is the authoritative 403; this avoids a pointless round-trip + leaking the tool exists).
|
|
265
|
+
if (tool.operator && !IS_OPERATOR)
|
|
266
|
+
return send({ jsonrpc: "2.0", id, error: { code: -32601, message: "unknown tool" } });
|
|
197
267
|
try {
|
|
198
268
|
if (!TOKEN)
|
|
199
269
|
throw new Error("JUJUGROWTH_TOKEN not set — generate one in Settings → Developer");
|