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/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]; // #67e8f9
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 /truecolor|24bit/i.test(process.env.COLORTERM ?? '') ||
20
- ['iTerm.app', 'vscode', 'WezTerm', 'ghostty'].includes(process.env.TERM_PROGRAM ?? '') ||
21
- Boolean(process.env.WT_SESSION);
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.map((ch, i) => {
43
- const t = chars.length === 1 ? 0 : i / (chars.length - 1);
44
- const [r, g, b] = VIOLET.map((v, k) => Math.round(v + (CYAN[k] - v) * t));
45
- return `\x1b[1m\x1b[38;2;${r};${g};${b}m${ch}`;
46
- }).join('');
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.map((ch, i) => {
51
- const step = ramp[Math.min(ramp.length - 1, Math.floor((i / chars.length) * ramp.length))];
52
- return `\x1b[1m\x1b[38;5;${step}m${ch}`;
53
- }).join('');
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(/("(?:[^"\\]|\\.)*")(\s*:)?|([{}[\],])/g, (m, str, colon, punct) => {
62
- if (punct) return c.dim(punct);
63
- if (colon !== undefined) return c.violet(str) + c.dim(colon);
64
- return c.cyan(str);
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
- try:
363
- body = await request.json()
364
- except Exception:
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
- try:
375
- body = await request.json()
376
- except Exception:
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
- try:
522
- body = await request.json()
523
- except Exception:
524
- body = {}
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: \"...\" })"})