jujugrowth-mcp 1.2.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 +54 -5
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
|
|
@@ -156,11 +165,47 @@ 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_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
|
+
},
|
|
164
209
|
];
|
|
165
210
|
|
|
166
211
|
function send(msg) {
|
|
@@ -199,15 +244,15 @@ async function handle(msg) {
|
|
|
199
244
|
}
|
|
200
245
|
if (method === "notifications/initialized") return; // no response to notifications
|
|
201
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".
|
|
202
249
|
return send({
|
|
203
250
|
jsonrpc: "2.0",
|
|
204
251
|
id,
|
|
205
252
|
result: {
|
|
206
|
-
tools: TOOLS.
|
|
207
|
-
name,
|
|
208
|
-
|
|
209
|
-
inputSchema,
|
|
210
|
-
})),
|
|
253
|
+
tools: TOOLS.filter((t) => IS_OPERATOR || !t.operator).map(
|
|
254
|
+
({ name, description, inputSchema }) => ({ name, description, inputSchema }),
|
|
255
|
+
),
|
|
211
256
|
},
|
|
212
257
|
});
|
|
213
258
|
}
|
|
@@ -215,6 +260,10 @@ async function handle(msg) {
|
|
|
215
260
|
const tool = TOOLS.find((t) => t.name === params?.name);
|
|
216
261
|
if (!tool)
|
|
217
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" } });
|
|
218
267
|
try {
|
|
219
268
|
if (!TOKEN)
|
|
220
269
|
throw new Error("JUJUGROWTH_TOKEN not set — generate one in Settings → Developer");
|