sparda-mcp 0.5.2 → 0.5.4
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 +158 -40
- package/SKILL.md +144 -0
- package/demo-app/package.json +7 -0
- package/demo-app/src/app.js +28 -0
- package/demo-app/src/routes/users.js +8 -0
- package/package.json +25 -3
- package/src/commands/demo.js +154 -0
- package/src/commands/doctor.js +51 -17
- package/src/commands/hook.js +21 -5
- package/src/commands/init.js +105 -33
- package/src/commands/remove.js +29 -10
- package/src/commands/report.js +372 -0
- package/src/commands/sync.js +37 -8
- package/src/detect.js +68 -18
- package/src/generator/express.js +86 -28
- package/src/generator/fastapi.js +66 -20
- package/src/generator/manifest.js +20 -13
- package/src/index.js +22 -2
- package/src/parser/express.js +144 -44
- package/src/parser/fastapi.js +13 -4
- package/src/probe/express-shim-esm.mjs +2 -2
- package/src/probe/express-shim.cjs +65 -22
- package/src/probe/integrate.js +34 -12
- package/src/probe/probe.js +59 -33
- package/src/probe/reconcile.js +24 -22
- package/src/security/sanitize.js +5 -1
- package/src/server/condenser.js +34 -12
- package/src/server/confirmation.js +75 -19
- package/src/server/context-carrier.js +36 -31
- package/src/server/crystallize.js +32 -9
- package/src/server/engine.js +118 -38
- package/src/server/idle.js +20 -4
- package/src/server/persistence.js +16 -5
- package/src/server/stdio.js +490 -162
- package/src/ui/style.js +30 -18
- package/templates/fastapi-router.txt +41 -13
package/src/ui/style.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// SPARDA identity: violet → cyan, same stops as the brand gradient
|
|
6
6
|
const VIOLET = [192, 132, 252]; // #c084fc
|
|
7
|
-
const CYAN = [103, 232, 249];
|
|
7
|
+
const CYAN = [103, 232, 249]; // #67e8f9
|
|
8
8
|
const RESET = '\x1b[0m';
|
|
9
9
|
|
|
10
10
|
// evaluated per call so tests (and runtime env changes) see the truth;
|
|
@@ -16,9 +16,13 @@ function enabled() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function truecolor() {
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
return (
|
|
20
|
+
/truecolor|24bit/i.test(process.env.COLORTERM ?? '') ||
|
|
21
|
+
['iTerm.app', 'vscode', 'WezTerm', 'ghostty'].includes(
|
|
22
|
+
process.env.TERM_PROGRAM ?? '',
|
|
23
|
+
) ||
|
|
24
|
+
Boolean(process.env.WT_SESSION)
|
|
25
|
+
);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const sgr = (open) => (s) => (enabled() ? `\x1b[${open}m${s}${RESET}` : String(s));
|
|
@@ -39,18 +43,23 @@ export function gradient(text) {
|
|
|
39
43
|
if (!enabled()) return text;
|
|
40
44
|
const chars = [...text];
|
|
41
45
|
if (truecolor()) {
|
|
42
|
-
const body = chars
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const body = chars
|
|
47
|
+
.map((ch, i) => {
|
|
48
|
+
const t = chars.length === 1 ? 0 : i / (chars.length - 1);
|
|
49
|
+
const [r, g, b] = VIOLET.map((v, k) => Math.round(v + (CYAN[k] - v) * t));
|
|
50
|
+
return `\x1b[1m\x1b[38;2;${r};${g};${b}m${ch}`;
|
|
51
|
+
})
|
|
52
|
+
.join('');
|
|
47
53
|
return body + RESET;
|
|
48
54
|
}
|
|
49
55
|
const ramp = [177, 141, 147, 153, 117];
|
|
50
|
-
const body = chars
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const body = chars
|
|
57
|
+
.map((ch, i) => {
|
|
58
|
+
const step =
|
|
59
|
+
ramp[Math.min(ramp.length - 1, Math.floor((i / chars.length) * ramp.length))];
|
|
60
|
+
return `\x1b[1m\x1b[38;5;${step}m${ch}`;
|
|
61
|
+
})
|
|
62
|
+
.join('');
|
|
54
63
|
return body + RESET;
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -58,9 +67,12 @@ export function gradient(text) {
|
|
|
58
67
|
// one pass on purpose: a second regex pass would chew the ANSI escapes of the first
|
|
59
68
|
export function colorizeJson(json) {
|
|
60
69
|
if (!enabled()) return json;
|
|
61
|
-
return json.replace(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
return json.replace(
|
|
71
|
+
/("(?:[^"\\]|\\.)*")(\s*:)?|([{}[\],])/g,
|
|
72
|
+
(m, str, colon, punct) => {
|
|
73
|
+
if (punct) return c.dim(punct);
|
|
74
|
+
if (colon !== undefined) return c.violet(str) + c.dim(colon);
|
|
75
|
+
return c.cyan(str);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
66
78
|
}
|
|
@@ -38,6 +38,9 @@ SPARDA_RECYCLE = {"servedByCircle": 0, "paidFull": 0}
|
|
|
38
38
|
# an explicit /invoke/confirm replays the token.
|
|
39
39
|
SPARDA_PENDING = {}
|
|
40
40
|
SPARDA_CONFIRM_TTL_MS = int(os.environ.get("SPARDA_CONFIRM_TTL_MS", "120000"))
|
|
41
|
+
# body cap on POST routes — mirrors express.json({ limit: '64kb' }) so neither
|
|
42
|
+
# framework lets an oversized payload pressure the host's memory (hard rule #1).
|
|
43
|
+
SPARDA_MAX_BODY = 64 * 1024
|
|
41
44
|
|
|
42
45
|
def sparda_nonce():
|
|
43
46
|
import base64
|
|
@@ -330,6 +333,34 @@ def sync_fetch(url, method, headers, body_bytes):
|
|
|
330
333
|
except Exception as e:
|
|
331
334
|
raise e
|
|
332
335
|
|
|
336
|
+
async def sparda_read_json(request):
|
|
337
|
+
# Bound the request body before parsing so an oversized POST can't pressure host
|
|
338
|
+
# memory. Streams with a hard cap so a chunked body (no Content-Length header) can't
|
|
339
|
+
# slip past either. Mirrors the Express side's express.json({ limit: '64kb' }).
|
|
340
|
+
# Returns (body, error_response): error_response is a 413 JSONResponse when too large,
|
|
341
|
+
# else None; a malformed or empty body yields ({}, None) like the old behavior.
|
|
342
|
+
cl = request.headers.get("content-length")
|
|
343
|
+
if cl is not None:
|
|
344
|
+
try:
|
|
345
|
+
if int(cl) > SPARDA_MAX_BODY:
|
|
346
|
+
return None, JSONResponse(status_code=413, content={"error": "payload too large"})
|
|
347
|
+
except ValueError:
|
|
348
|
+
pass
|
|
349
|
+
total = 0
|
|
350
|
+
chunks = []
|
|
351
|
+
async for chunk in request.stream():
|
|
352
|
+
total += len(chunk)
|
|
353
|
+
if total > SPARDA_MAX_BODY:
|
|
354
|
+
return None, JSONResponse(status_code=413, content={"error": "payload too large"})
|
|
355
|
+
chunks.append(chunk)
|
|
356
|
+
raw = b"".join(chunks)
|
|
357
|
+
if not raw:
|
|
358
|
+
return {}, None
|
|
359
|
+
try:
|
|
360
|
+
return json.loads(raw), None
|
|
361
|
+
except Exception:
|
|
362
|
+
return {}, None
|
|
363
|
+
|
|
333
364
|
@sparda_router.get("/tools")
|
|
334
365
|
async def get_tools(request: Request):
|
|
335
366
|
if request.headers.get("x-sparda-key") != SPARDA_LOCAL_KEY:
|
|
@@ -359,10 +390,9 @@ async def gossip(request: Request):
|
|
|
359
390
|
# or malformed count is silently dropped (bounded, no injection).
|
|
360
391
|
if request.headers.get("x-sparda-key") != SPARDA_LOCAL_KEY:
|
|
361
392
|
return JSONResponse(status_code=401, content={"error": "unauthorized"})
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
body = {}
|
|
393
|
+
body, err = await sparda_read_json(request)
|
|
394
|
+
if err is not None:
|
|
395
|
+
return err
|
|
366
396
|
sparda_merge_gossip(body)
|
|
367
397
|
return Response(status_code=204)
|
|
368
398
|
|
|
@@ -371,10 +401,9 @@ async def invoke_tool(request: Request):
|
|
|
371
401
|
if request.headers.get("x-sparda-key") != SPARDA_LOCAL_KEY:
|
|
372
402
|
return JSONResponse(status_code=401, content={"error": "unauthorized"})
|
|
373
403
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
body = {}
|
|
404
|
+
body, err = await sparda_read_json(request)
|
|
405
|
+
if err is not None:
|
|
406
|
+
return err
|
|
378
407
|
|
|
379
408
|
tool = body.get("tool")
|
|
380
409
|
args = body.get("args", {})
|
|
@@ -518,11 +547,10 @@ async def confirm_tool_invocation(request: Request):
|
|
|
518
547
|
return JSONResponse(status_code=401, content={"error": "unauthorized"})
|
|
519
548
|
|
|
520
549
|
t0 = time.time()
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
550
|
+
body, err = await sparda_read_json(request)
|
|
551
|
+
if err is not None:
|
|
552
|
+
return err
|
|
553
|
+
|
|
526
554
|
confirm = body.get("confirm")
|
|
527
555
|
if not isinstance(confirm, str) or not confirm:
|
|
528
556
|
return JSONResponse(status_code=400, content={"error": "missing confirm token (expected { confirm: \"...\" })"})
|