memstack-skill-loader 4.0.4__tar.gz → 4.0.6__tar.gz

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.
Files changed (28) hide show
  1. {memstack_skill_loader-4.0.4/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.0.6}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/agent_runner.py +125 -12
  5. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/dashboard.html +217 -4
  6. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/dashboard.py +68 -2
  7. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  8. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/MANIFEST.in +0 -0
  9. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/README.md +0 -0
  10. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/setup.cfg +0 -0
  11. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/__main__.py +0 -0
  12. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/categories.py +0 -0
  13. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/compression.py +0 -0
  14. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/config.py +0 -0
  15. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/indexer.py +0 -0
  16. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/license.py +0 -0
  17. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/memory_db.py +0 -0
  18. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/search.py +0 -0
  19. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/server.py +0 -0
  20. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/skill_config.py +0 -0
  21. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/stats.py +0 -0
  22. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/tfidf_search.py +0 -0
  23. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader/version_check.py +0 -0
  24. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
  25. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  26. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  27. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  28. {memstack_skill_loader-4.0.4 → memstack_skill_loader-4.0.6}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.4
3
+ Version: 4.0.6
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.0.4"
7
+ version = "4.0.6"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.0.4"
3
+ __version__ = "4.0.6"
@@ -66,7 +66,8 @@ SYSTEM_PROMPTS = {
66
66
  "external stylesheet.\n"
67
67
  "- Never hardcode secrets, API keys, tokens, passwords, or credentials in source code. "
68
68
  "Always use environment variables loaded from .env files. If a secret is needed, create "
69
- "or update a .env.example file with the variable name and a placeholder value."
69
+ "or update a .env.example file with the variable name and a placeholder value.\n"
70
+ "- Do NOT run git add, git commit, or any git commands. Only make file changes."
70
71
  ),
71
72
  "reviewer": (
72
73
  "You are a Reviewer agent. You review the Builder's changes against the original "
@@ -88,6 +89,49 @@ SYSTEM_PROMPTS = {
88
89
  ),
89
90
  }
90
91
 
92
+ BUILDER_TOOLS_CONFIG_FILE = Path.home() / ".memstack" / "builder-tools-config.json"
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # MCP server discovery & Builder tools config
97
+ # ---------------------------------------------------------------------------
98
+
99
+ def discover_mcp_servers(workdir: str) -> list[str]:
100
+ """Read .mcp.json from workdir and return the list of MCP server name keys."""
101
+ try:
102
+ mcp_file = Path(workdir) / ".mcp.json"
103
+ if not mcp_file.is_file():
104
+ return []
105
+ data = json.loads(mcp_file.read_text(encoding="utf-8"))
106
+ servers = data.get("mcpServers", {})
107
+ return sorted(servers.keys()) if isinstance(servers, dict) else []
108
+ except Exception:
109
+ return []
110
+
111
+
112
+ def load_builder_tools_config() -> dict:
113
+ """Load per-project blocked MCP server lists from ~/.memstack/builder-tools-config.json."""
114
+ try:
115
+ if BUILDER_TOOLS_CONFIG_FILE.is_file():
116
+ return json.loads(BUILDER_TOOLS_CONFIG_FILE.read_text(encoding="utf-8"))
117
+ except Exception:
118
+ pass
119
+ return {}
120
+
121
+
122
+ def save_builder_tools_config(config: dict) -> None:
123
+ """Save per-project blocked MCP server config."""
124
+ BUILDER_TOOLS_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
125
+ BUILDER_TOOLS_CONFIG_FILE.write_text(json.dumps(config, indent=2), encoding="utf-8")
126
+
127
+
128
+ def get_blocked_servers_for_project(workdir: str) -> list[str]:
129
+ """Return blocked MCP servers for a project, falling back to global defaults."""
130
+ cfg = load_builder_tools_config()
131
+ if workdir in cfg:
132
+ return cfg[workdir]
133
+ return cfg.get("_global_defaults", [])
134
+
91
135
 
92
136
  # ---------------------------------------------------------------------------
93
137
  # Project context gathering
@@ -259,6 +303,30 @@ def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int]:
259
303
  return text, input_tokens, output_tokens, cost_usd, context_tokens
260
304
 
261
305
 
306
+ def _extract_text_from_stream_line(line: str) -> Optional[str]:
307
+ """Extract user-visible text from a single stream-json line.
308
+
309
+ Returns the text if the line contains assistant text or a result, else None.
310
+ """
311
+ try:
312
+ obj = json.loads(line.strip())
313
+ except (json.JSONDecodeError, ValueError):
314
+ return None
315
+ msg_type = obj.get("type")
316
+ if msg_type == "assistant":
317
+ parts = []
318
+ for block in obj.get("message", {}).get("content", []):
319
+ if block.get("type") == "text":
320
+ t = block.get("text", "")
321
+ if t:
322
+ parts.append(t)
323
+ return "\n".join(parts) if parts else None
324
+ if msg_type == "result":
325
+ t = obj.get("result", "")
326
+ return t if t else None
327
+ return None
328
+
329
+
262
330
  # ---------------------------------------------------------------------------
263
331
  # Direct API invocation (Manager / Reviewer — no file tools needed)
264
332
  # ---------------------------------------------------------------------------
@@ -359,7 +427,8 @@ def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
359
427
  def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[Path] = None,
360
428
  skip_permissions: bool = False,
361
429
  session_id: Optional[str] = None, timeout: int = AGENT_TIMEOUT,
362
- model: str = "") -> tuple[str, int, int, int]:
430
+ model: str = "", disallowed_tools: Optional[list[str]] = None,
431
+ session: Optional["Session"] = None) -> tuple[str, int, int, int]:
363
432
  """Run a single claude --print invocation and return the output."""
364
433
  claude_bin = shutil.which("claude")
365
434
  if not claude_bin:
@@ -370,6 +439,9 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
370
439
  cmd.append("--dangerously-skip-permissions")
371
440
  if model:
372
441
  cmd.extend(["--model", model])
442
+ if disallowed_tools:
443
+ patterns = ",".join(f"mcp__{srv}__*" for srv in disallowed_tools)
444
+ cmd.extend(["--disallowedTools", patterns])
373
445
 
374
446
  if log_path:
375
447
  log_path.parent.mkdir(parents=True, exist_ok=True)
@@ -393,9 +465,6 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
393
465
  stdout=subprocess.PIPE,
394
466
  stderr=subprocess.PIPE,
395
467
  cwd=working_dir,
396
- text=True,
397
- encoding="utf-8",
398
- errors="replace",
399
468
  env=_build_env(),
400
469
  creationflags=0,
401
470
  )
@@ -413,12 +482,38 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
413
482
  killed_by_watchdog.set()
414
483
  proc.kill()
415
484
 
485
+ stdout_chunks: list[str] = []
486
+ stderr_chunks: list[str] = []
487
+ partial_text: list[str] = []
488
+
489
+ def _read_stdout() -> None:
490
+ for raw_line in proc.stdout:
491
+ line = raw_line.decode("utf-8", errors="replace")
492
+ stdout_chunks.append(line)
493
+ extracted = _extract_text_from_stream_line(line)
494
+ if extracted and session is not None:
495
+ partial_text.append(extracted)
496
+ try:
497
+ session.agents[name]["last_output"] = "".join(partial_text)[-500:]
498
+ session._save_state()
499
+ except Exception:
500
+ pass
501
+
502
+ def _read_stderr() -> None:
503
+ for raw_line in proc.stderr:
504
+ stderr_chunks.append(raw_line.decode("utf-8", errors="replace"))
505
+
506
+ t_out = threading.Thread(target=_read_stdout, daemon=True)
507
+ t_err = threading.Thread(target=_read_stderr, daemon=True)
508
+ t_out.start()
509
+ t_err.start()
510
+
416
511
  watchdog = threading.Timer(timeout, _watchdog_kill)
417
512
  watchdog.daemon = True
418
513
  watchdog.start()
419
514
 
420
515
  try:
421
- stdout_data, stderr_data = proc.communicate()
516
+ proc.wait()
422
517
  except Exception as e:
423
518
  watchdog.cancel()
424
519
  with _lock:
@@ -426,20 +521,26 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
426
521
  _current_process = None
427
522
  if log_path:
428
523
  with open(log_path, "a", encoding="utf-8") as f:
429
- f.write(f"[{ts}] communicate() error: {e}\n")
430
- raise RuntimeError(f"{name} communicate() failed: {e}") from e
524
+ f.write(f"[{ts}] wait() error: {e}\n")
525
+ raise RuntimeError(f"{name} wait() failed: {e}") from e
431
526
  finally:
432
527
  watchdog.cancel()
433
528
  with _lock:
434
529
  if _current_process is proc:
435
530
  _current_process = None
436
531
 
532
+ t_out.join(timeout=10)
533
+ t_err.join(timeout=10)
534
+
437
535
  if killed_by_watchdog.is_set():
438
536
  raise subprocess.TimeoutExpired(cmd, timeout)
439
537
 
538
+ stdout_data = "".join(stdout_chunks)
539
+ stderr_data = "".join(stderr_chunks)
540
+
440
541
  if log_path:
441
542
  with open(log_path, "a", encoding="utf-8") as f:
442
- f.write(f"[{ts}] communicate() returned: stdout={len(stdout_data)} stderr={len(stderr_data)}\n")
543
+ f.write(f"[{ts}] Process exited: stdout={len(stdout_data)} stderr={len(stderr_data)}\n")
443
544
 
444
545
  raw_stdout = stdout_data or ""
445
546
  stderr = stderr_data or ""
@@ -485,7 +586,8 @@ class Session:
485
586
  """Tracks the full state of an agent run session."""
486
587
 
487
588
  def __init__(self, task: str, working_dir: str, context: Optional[str] = None, auto_commit: bool = False, timeout_minutes: int = 60,
488
- manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = ""):
589
+ manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = "",
590
+ blocked_mcp_servers: Optional[list[str]] = None):
489
591
  self.session_id = uuid.uuid4().hex[:12]
490
592
  self.task = task
491
593
  self.working_dir = working_dir
@@ -493,6 +595,7 @@ class Session:
493
595
  self.user_name = user_name
494
596
  self.context = context
495
597
  self.auto_commit = auto_commit
598
+ self.blocked_mcp_servers = blocked_mcp_servers or []
496
599
  self.auto_committed = False
497
600
  self.timeout = max(5, min(120, timeout_minutes)) * 60
498
601
  self.status = "running"
@@ -607,6 +710,7 @@ def _orchestrate(session: Session) -> None:
607
710
  # Step 1: Manager analyzes the task
608
711
  session.agents["manager"]["status"] = "busy"
609
712
  session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
713
+ session.agents["manager"]["last_output"] = ""
610
714
  session._save_state()
611
715
 
612
716
  context = _gather_project_context(session.working_dir)
@@ -666,6 +770,7 @@ def _orchestrate(session: Session) -> None:
666
770
  # Builder
667
771
  session.agents["builder"]["status"] = "busy"
668
772
  session.agents["builder"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
773
+ session.agents["builder"]["last_output"] = ""
669
774
  session._save_state()
670
775
 
671
776
  builder_prompt = (
@@ -684,6 +789,8 @@ def _orchestrate(session: Session) -> None:
684
789
  skip_permissions=True, session_id=session.session_id,
685
790
  timeout=session.timeout,
686
791
  model=session.models.get("builder", ""),
792
+ disallowed_tools=session.blocked_mcp_servers,
793
+ session=session,
687
794
  )
688
795
  except subprocess.TimeoutExpired:
689
796
  session.agents["builder"]["status"] = "timeout"
@@ -733,6 +840,7 @@ def _orchestrate(session: Session) -> None:
733
840
  # Reviewer
734
841
  session.agents["reviewer"]["status"] = "busy"
735
842
  session.agents["reviewer"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
843
+ session.agents["reviewer"]["last_output"] = ""
736
844
  session._save_state()
737
845
 
738
846
  reviewer_prompt = (
@@ -876,7 +984,8 @@ _BLOCKED_NIX = {"/etc", "/var", "/proc", "/sys", "/boot"}
876
984
 
877
985
 
878
986
  def start_run(task: str, working_dir: Optional[str] = None, context: Optional[str] = None, auto_commit: bool = True, timeout_minutes: int = 60,
879
- manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = "") -> dict:
987
+ manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = "",
988
+ blocked_mcp_servers: Optional[list[str]] = None) -> dict:
880
989
  """Start a new agent run. Returns session info."""
881
990
  global _current_session, _orchestration_thread
882
991
 
@@ -895,12 +1004,16 @@ def start_run(task: str, working_dir: Optional[str] = None, context: Optional[st
895
1004
  if any(str_wd == bp or str_wd.startswith(bp + "/") for bp in _BLOCKED_NIX):
896
1005
  return {"error": f"Working directory not allowed: {wd}"}
897
1006
 
1007
+ if blocked_mcp_servers is None:
1008
+ blocked_mcp_servers = get_blocked_servers_for_project(str(wd))
1009
+
898
1010
  with _lock:
899
1011
  if _current_session and _current_session.status == "running":
900
1012
  return {"error": "A session is already running", "session_id": _current_session.session_id}
901
1013
 
902
1014
  session = Session(task=task, working_dir=working_dir, context=context, auto_commit=auto_commit, timeout_minutes=timeout_minutes,
903
- manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name)
1015
+ manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name,
1016
+ blocked_mcp_servers=blocked_mcp_servers)
904
1017
  _current_session = session
905
1018
 
906
1019
  session._save_state()
@@ -835,6 +835,76 @@
835
835
  font-size: 0.82rem;
836
836
  }
837
837
 
838
+ /* ── Builder MCP Tools section ── */
839
+ .mcp-tools-details {
840
+ margin-bottom: 0.8rem;
841
+ }
842
+ .mcp-tools-summary {
843
+ cursor: pointer;
844
+ font-size: 0.82rem;
845
+ color: #8b949e;
846
+ font-weight: 600;
847
+ list-style: none;
848
+ display: flex;
849
+ align-items: center;
850
+ gap: 0.4rem;
851
+ }
852
+ .mcp-tools-summary::-webkit-details-marker { display: none; }
853
+ .mcp-tools-summary::before {
854
+ content: '▶';
855
+ font-size: 0.6rem;
856
+ transition: transform 0.2s;
857
+ }
858
+ .mcp-tools-details[open] > .mcp-tools-summary::before {
859
+ transform: rotate(90deg);
860
+ }
861
+ .mcp-tools-summary:hover { color: #c9d1d9; }
862
+ .mcp-tools-list {
863
+ margin-top: 0.5rem;
864
+ display: flex;
865
+ flex-direction: column;
866
+ gap: 0.3rem;
867
+ }
868
+ .mcp-tools-row {
869
+ display: flex;
870
+ align-items: center;
871
+ gap: 0.5rem;
872
+ font-size: 0.82rem;
873
+ color: #c9d1d9;
874
+ }
875
+ .mcp-tools-row input[type="checkbox"] {
876
+ accent-color: #238636;
877
+ width: 14px;
878
+ height: 14px;
879
+ cursor: pointer;
880
+ flex-shrink: 0;
881
+ }
882
+ .mcp-tools-row label { cursor: pointer; font-family: monospace; }
883
+ .mcp-tools-hint {
884
+ font-size: 0.72rem;
885
+ color: #484f58;
886
+ margin-top: 0.4rem;
887
+ }
888
+ .mcp-tools-save-btn {
889
+ margin-top: 0.5rem;
890
+ background: #21262d;
891
+ color: #8b949e;
892
+ border: 1px solid #30363d;
893
+ padding: 0.3rem 0.8rem;
894
+ border-radius: 6px;
895
+ font-size: 0.75rem;
896
+ cursor: pointer;
897
+ transition: background 0.2s, color 0.2s;
898
+ }
899
+ .mcp-tools-save-btn:hover { background: #30363d; color: #c9d1d9; }
900
+ .mcp-tools-saved {
901
+ font-size: 0.72rem;
902
+ color: #39d353;
903
+ margin-left: 0.5rem;
904
+ opacity: 0;
905
+ transition: opacity 0.3s;
906
+ }
907
+
838
908
  /* ── Dancing status phrases ── */
839
909
  .agent-status-phrase {
840
910
  transition: opacity 0.3s ease;
@@ -1431,7 +1501,7 @@
1431
1501
  <label style="display:block;font-size:0.82rem;color:#8b949e;margin-bottom:0.4rem;font-weight:600;">Task Description</label>
1432
1502
  <div style="position:relative;margin-bottom:1rem;">
1433
1503
  <textarea id="agent-task-input" rows="4" placeholder="Describe what you want the agents to build..." style="width:100%;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;padding:0.7rem;font-size:0.88rem;resize:vertical;font-family:inherit;margin:0;"></textarea>
1434
- <button onclick="document.getElementById('agent-task-input').value='';document.getElementById('agent-workdir-input').value='';document.getElementById('agent-context-input').value='';document.getElementById('agent-task-input').focus();" style="position:absolute;top:4px;right:4px;background:#21262d;border:1px solid #30363d;color:#8b949e;padding:8px;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;z-index:1;" onmouseenter="this.style.color='#e6edf3'" onmouseleave="this.style.color='#8b949e'" title="Clear task and working directory">&times;</button>
1504
+ <button onclick="document.getElementById('agent-task-input').value='';document.getElementById('agent-workdir-input').value='';document.getElementById('agent-context-input').value='';fetchMcpServers('');document.getElementById('agent-task-input').focus();" style="position:absolute;top:4px;right:4px;background:#21262d;border:1px solid #30363d;color:#8b949e;padding:8px;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;z-index:1;" onmouseenter="this.style.color='#e6edf3'" onmouseleave="this.style.color='#8b949e'" title="Clear task and working directory">&times;</button>
1435
1505
  </div>
1436
1506
  <label style="display:block;font-size:0.82rem;color:#8b949e;margin-bottom:0.4rem;font-weight:600;">Working Directory</label>
1437
1507
  <div style="display:flex;gap:0.5rem;margin-bottom:0.3rem;">
@@ -1474,6 +1544,20 @@
1474
1544
  </div>
1475
1545
  </div>
1476
1546
  </details>
1547
+ <details class="mcp-tools-details" id="mcp-tools-details">
1548
+ <summary class="mcp-tools-summary" id="mcp-tools-summary">Builder MCP Tools</summary>
1549
+ <div id="mcp-tools-container">
1550
+ <div id="mcp-tools-loading" style="font-size:0.78rem;color:#484f58;padding:0.3rem 0;">Enter a working directory to discover MCP servers</div>
1551
+ <div id="mcp-tools-list" class="mcp-tools-list" style="display:none;"></div>
1552
+ <div id="mcp-tools-empty" style="display:none;font-size:0.78rem;color:#484f58;padding:0.3rem 0;">No MCP servers found in this project</div>
1553
+ <p class="mcp-tools-hint" id="mcp-tools-hint" style="display:none;">Unchecked servers will be excluded from the Builder's context to reduce token usage</p>
1554
+ <p id="mcp-tools-global-note" style="display:none;font-size:0.72rem;color:#d29922;margin:0.3rem 0 0;">Using global defaults</p>
1555
+ <div id="mcp-tools-actions" style="display:none;margin-top:0.4rem;">
1556
+ <button class="mcp-tools-save-btn" onclick="saveMcpToolsConfig()">Save as Default</button>
1557
+ <span class="mcp-tools-saved" id="mcp-tools-saved">Saved</span>
1558
+ </div>
1559
+ </div>
1560
+ </details>
1477
1561
  <label for="agent-autocommit-checkbox" style="display:flex;align-items:center;gap:0.5rem;font-size:0.82rem;color:#8b949e;margin-bottom:1rem;cursor:pointer;position:relative;z-index:1;">
1478
1562
  <input id="agent-autocommit-checkbox" type="checkbox" style="accent-color:#238636;width:15px;height:15px;cursor:pointer;flex-shrink:0;">
1479
1563
  Auto-commit on approval
@@ -1658,6 +1742,22 @@
1658
1742
  </div>
1659
1743
  </div>
1660
1744
 
1745
+ <div class="panel">
1746
+ <details class="mcp-tools-details">
1747
+ <summary class="mcp-tools-summary">Default MCP Tools</summary>
1748
+ <div style="padding:0.3rem 0;">
1749
+ <p style="color:#8b949e;font-size:0.78rem;margin:0 0 0.6rem;">Set which MCP servers are blocked by default across all projects. Per-project settings override these defaults.</p>
1750
+ <label style="font-size:0.78rem;color:#8b949e;display:block;margin-bottom:0.3rem;">Blocked servers (comma-separated)</label>
1751
+ <input type="text" id="settings-global-mcp-blocked" class="settings-text-input" placeholder="e.g. agent-bridge, connectstack-uptimerobot">
1752
+ <div style="display:flex;gap:0.5rem;margin-top:0.5rem;align-items:center;">
1753
+ <button class="mcp-tools-save-btn" onclick="saveGlobalMcpDefaults(false)">Save Defaults</button>
1754
+ <button class="mcp-tools-save-btn" style="background:#30363d;" onclick="applyGlobalMcpToAll()">Apply to All Projects</button>
1755
+ <span id="settings-global-mcp-saved" class="mcp-tools-saved">Saved</span>
1756
+ </div>
1757
+ </div>
1758
+ </details>
1759
+ </div>
1760
+
1661
1761
  <div class="panel">
1662
1762
  <h3>Dashboard Info</h3>
1663
1763
  <div style="display:grid;grid-template-columns:auto 1fr;gap:0.4rem 1.2rem;font-size:0.82rem;">
@@ -2819,6 +2919,8 @@ async function startAgentTask() {
2819
2919
  body.builder_model = document.getElementById('agent-model-builder').value;
2820
2920
  body.reviewer_model = document.getElementById('agent-model-reviewer').value;
2821
2921
  body.user_name = userProfile.user_name || '';
2922
+ const blockedMcp = getBlockedMcpServers();
2923
+ if (blockedMcp.length) body.blocked_mcp_servers = blockedMcp;
2822
2924
  const res = await fetch('/api/agent-run', {method:'POST', headers: AUTH_HEADERS, body: JSON.stringify(body)});
2823
2925
  const data = await res.json();
2824
2926
  if (data.error) { alert(data.error); return; }
@@ -2857,6 +2959,12 @@ async function loadAgentMonitor() {
2857
2959
  lbl.textContent = userProfile.agent_names[role];
2858
2960
  }
2859
2961
  }
2962
+ updateMcpToolsSummaryLabel();
2963
+ const wdInput = document.getElementById('agent-workdir-input');
2964
+ if (wdInput && !wdInput._mcpBound) {
2965
+ wdInput._mcpBound = true;
2966
+ wdInput.addEventListener('blur', () => fetchMcpServers(wdInput.value.trim()));
2967
+ }
2860
2968
  fetchAgentStatus();
2861
2969
  loadRecentProjects();
2862
2970
  loadLastWorkdir();
@@ -2872,10 +2980,110 @@ async function loadLastWorkdir() {
2872
2980
  if (input.value.trim()) return;
2873
2981
  const res = await fetch('/api/last-workdir', {headers: {'X-Auth-Token': AUTH_TOKEN}});
2874
2982
  const data = await res.json();
2875
- if (data.path) input.value = data.path;
2983
+ if (data.path) { input.value = data.path; fetchMcpServers(data.path); }
2876
2984
  } catch(e) { /* ignore */ }
2877
2985
  }
2878
2986
 
2987
+ /* ─── Builder MCP Tools ─── */
2988
+ let _mcpServers = [];
2989
+ let _mcpBlocked = new Set();
2990
+
2991
+ function updateMcpToolsSummaryLabel() {
2992
+ const el = document.getElementById('mcp-tools-summary');
2993
+ if (!el) return;
2994
+ const name = (userProfile.agent_names && userProfile.agent_names.builder) || 'Builder';
2995
+ el.textContent = name + ' MCP Tools';
2996
+ }
2997
+
2998
+ async function fetchMcpServers(workdir) {
2999
+ const listEl = document.getElementById('mcp-tools-list');
3000
+ const emptyEl = document.getElementById('mcp-tools-empty');
3001
+ const loadingEl = document.getElementById('mcp-tools-loading');
3002
+ const hintEl = document.getElementById('mcp-tools-hint');
3003
+ const actionsEl = document.getElementById('mcp-tools-actions');
3004
+ if (!workdir) {
3005
+ listEl.style.display = 'none'; emptyEl.style.display = 'none'; hintEl.style.display = 'none'; actionsEl.style.display = 'none';
3006
+ const gn = document.getElementById('mcp-tools-global-note'); if (gn) gn.style.display = 'none';
3007
+ loadingEl.style.display = ''; loadingEl.textContent = 'Enter a working directory to discover MCP servers';
3008
+ _mcpServers = []; _mcpBlocked = new Set();
3009
+ return;
3010
+ }
3011
+ const globalNoteEl = document.getElementById('mcp-tools-global-note');
3012
+ loadingEl.style.display = ''; loadingEl.textContent = 'Discovering MCP servers...';
3013
+ listEl.style.display = 'none'; emptyEl.style.display = 'none'; hintEl.style.display = 'none'; actionsEl.style.display = 'none';
3014
+ if (globalNoteEl) globalNoteEl.style.display = 'none';
3015
+ try {
3016
+ const res = await fetch('/api/mcp-servers?workdir=' + encodeURIComponent(workdir), {headers: AUTH_GET});
3017
+ const data = await res.json();
3018
+ _mcpServers = data.servers || [];
3019
+ _mcpBlocked = new Set(data.blocked || []);
3020
+ loadingEl.style.display = 'none';
3021
+ if (_mcpServers.length === 0) {
3022
+ emptyEl.style.display = ''; return;
3023
+ }
3024
+ listEl.innerHTML = _mcpServers.map(srv => {
3025
+ const checked = !_mcpBlocked.has(srv) ? 'checked' : '';
3026
+ const id = 'mcp-srv-' + srv.replace(/[^a-zA-Z0-9_-]/g, '_');
3027
+ return `<div class="mcp-tools-row"><input type="checkbox" id="${id}" value="${escapeHtml(srv)}" ${checked}><label for="${id}">${escapeHtml(srv)}</label></div>`;
3028
+ }).join('');
3029
+ listEl.style.display = ''; hintEl.style.display = ''; actionsEl.style.display = '';
3030
+ if (globalNoteEl && !data.has_project_config && (data.global_blocked || []).length > 0) {
3031
+ globalNoteEl.style.display = '';
3032
+ }
3033
+ } catch(e) {
3034
+ loadingEl.style.display = 'none'; emptyEl.style.display = ''; emptyEl.textContent = 'Failed to discover MCP servers';
3035
+ }
3036
+ }
3037
+
3038
+ function getBlockedMcpServers() {
3039
+ const blocked = [];
3040
+ for (const srv of _mcpServers) {
3041
+ const id = 'mcp-srv-' + srv.replace(/[^a-zA-Z0-9_-]/g, '_');
3042
+ const cb = document.getElementById(id);
3043
+ if (cb && !cb.checked) blocked.push(srv);
3044
+ }
3045
+ return blocked;
3046
+ }
3047
+
3048
+ async function saveMcpToolsConfig() {
3049
+ const workdir = document.getElementById('agent-workdir-input').value.trim();
3050
+ if (!workdir) return;
3051
+ const blocked = getBlockedMcpServers();
3052
+ try {
3053
+ await fetch('/api/builder-tools-config', {method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify({workdir, blocked})});
3054
+ const savedEl = document.getElementById('mcp-tools-saved');
3055
+ savedEl.style.opacity = '1';
3056
+ setTimeout(() => { savedEl.style.opacity = '0'; }, 2000);
3057
+ } catch(e) { /* ignore */ }
3058
+ }
3059
+
3060
+ async function loadGlobalMcpDefaults() {
3061
+ try {
3062
+ const res = await fetch('/api/global-mcp-defaults', {headers: AUTH_GET});
3063
+ const d = await res.json();
3064
+ const el = document.getElementById('settings-global-mcp-blocked');
3065
+ if (el) el.value = (d.blocked || []).join(', ');
3066
+ } catch(e) { /* ignore */ }
3067
+ }
3068
+
3069
+ async function saveGlobalMcpDefaults(applyAll) {
3070
+ const el = document.getElementById('settings-global-mcp-blocked');
3071
+ if (!el) return;
3072
+ const blocked = el.value.split(',').map(s => s.trim()).filter(Boolean);
3073
+ try {
3074
+ await fetch('/api/global-mcp-defaults', {method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify({blocked, apply_all: !!applyAll})});
3075
+ const savedEl = document.getElementById('settings-global-mcp-saved');
3076
+ savedEl.textContent = applyAll ? 'Applied to all' : 'Saved';
3077
+ savedEl.style.opacity = '1';
3078
+ setTimeout(() => { savedEl.style.opacity = '0'; }, 2000);
3079
+ } catch(e) { /* ignore */ }
3080
+ }
3081
+
3082
+ function applyGlobalMcpToAll() {
3083
+ if (!confirm('This will overwrite all per-project MCP settings. Continue?')) return;
3084
+ saveGlobalMcpDefaults(true);
3085
+ }
3086
+
2879
3087
  function autoResize(textarea) {
2880
3088
  textarea.style.height = 'auto';
2881
3089
  textarea.style.height = textarea.scrollHeight + 'px';
@@ -2984,7 +3192,7 @@ async function loadRecentProjects() {
2984
3192
  if (!dirs.length) { dropdown.style.display = 'none'; return; }
2985
3193
  dropdown.style.display = 'block';
2986
3194
  dropdown.innerHTML = dirs.map(d =>
2987
- `<div style="padding:0.4rem 0.7rem;cursor:pointer;font-size:0.8rem;font-family:monospace;color:#8b949e;border-bottom:1px solid #21262d;transition:background 0.15s;" onmouseenter="this.style.background='#161b22';this.style.color='#e6edf3'" onmouseleave="this.style.background='';this.style.color='#8b949e'" onclick="document.getElementById('agent-workdir-input').value=this.textContent">${escapeHtml(d)}</div>`
3195
+ `<div style="padding:0.4rem 0.7rem;cursor:pointer;font-size:0.8rem;font-family:monospace;color:#8b949e;border-bottom:1px solid #21262d;transition:background 0.15s;" onmouseenter="this.style.background='#161b22';this.style.color='#e6edf3'" onmouseleave="this.style.background='';this.style.color='#8b949e'" onclick="document.getElementById('agent-workdir-input').value=this.textContent;fetchMcpServers(this.textContent)">${escapeHtml(d)}</div>`
2988
3196
  ).join('');
2989
3197
  } catch(e) { /* ignore */ }
2990
3198
  }
@@ -3006,6 +3214,7 @@ function closeDirBrowser() {
3006
3214
  function selectDirBrowser() {
3007
3215
  document.getElementById('agent-workdir-input').value = dirBrowserCurrentPath;
3008
3216
  closeDirBrowser();
3217
+ fetchMcpServers(dirBrowserCurrentPath);
3009
3218
  }
3010
3219
 
3011
3220
  function dirBrowserBack() {
@@ -3245,6 +3454,7 @@ function renderAgentUI(data) {
3245
3454
  const pulseStyle = a.status === 'busy' ? 'animation:pulse-busy 2s infinite;' : '';
3246
3455
  const roleIcon = role === 'manager' ? '&#128188;' : role === 'builder' ? '&#128736;' : '&#128269;';
3247
3456
  let statusDisplay;
3457
+ const hasStreamOutput = a.status === 'busy' && a.last_output && a.last_output.trim().length > 0;
3248
3458
  if (a.status === 'busy' && BUSY_PHRASES[role]) {
3249
3459
  const phrase = BUSY_PHRASES[role][busyPhraseIndices[role] % BUSY_PHRASES[role].length];
3250
3460
  const customName = (userProfile.agent_names && userProfile.agent_names[role]) || role.charAt(0).toUpperCase() + role.slice(1);
@@ -3290,6 +3500,8 @@ function renderAgentUI(data) {
3290
3500
  }
3291
3501
  }
3292
3502
  const errorBorder = (a.status === 'error' || a.status === 'timeout' || a.status === 'crashed') ? ' agent-card-error-border' : '';
3503
+ const outputMaxHeight = hasStreamOutput ? '6em' : '3em';
3504
+ const outputSnippet = escapeHtml((a.last_output || '').substring(0, hasStreamOutput ? 500 : 150));
3293
3505
  return `<div class="agent-card${errorBorder}" style="${pulseStyle}">
3294
3506
  <span class="agent-tooltip">${escapeHtml(ROLE_DESCRIPTIONS[role] || '')}</span>
3295
3507
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.6rem;">
@@ -3297,7 +3509,7 @@ function renderAgentUI(data) {
3297
3509
  <span style="background:${color}20;color:${color};padding:2px 8px;border-radius:12px;font-size:0.7rem;font-weight:600;text-transform:uppercase;">${statusDisplay}</span>
3298
3510
  </div>
3299
3511
  <div style="font-size:0.75rem;color:#8b949e;line-height:1.6;">
3300
- <div style="margin-top:0.3rem;color:#c9d1d9;font-size:0.72rem;max-height:3em;overflow:hidden;text-overflow:ellipsis;">${escapeHtml((a.last_output || '').substring(0, 150))}</div>
3512
+ <div style="margin-top:0.3rem;color:#c9d1d9;font-size:0.72rem;max-height:${outputMaxHeight};overflow:hidden;text-overflow:ellipsis;white-space:pre-wrap;word-break:break-word;">${outputSnippet}</div>
3301
3513
  </div>
3302
3514
  ${elapsedHtml}
3303
3515
  ${contextHtml}
@@ -3512,6 +3724,7 @@ async function loadSettings() {
3512
3724
 
3513
3725
  loadModelPrefs();
3514
3726
  await loadUserProfile();
3727
+ loadGlobalMcpDefaults();
3515
3728
  settingsLoaded = true;
3516
3729
  } catch (e) {
3517
3730
  console.error('Settings load failed:', e);
@@ -458,6 +458,27 @@ class _Handler(BaseHTTPRequestHandler):
458
458
  pass
459
459
  body = json.dumps({"path": path}).encode()
460
460
  self._respond(200, "application/json", body)
461
+ elif self.path.startswith("/api/mcp-servers"):
462
+ parsed = urlparse(self.path)
463
+ qs = parse_qs(parsed.query)
464
+ workdir = qs.get("workdir", [""])[0]
465
+ servers = agent_runner.discover_mcp_servers(workdir) if workdir else []
466
+ cfg = agent_runner.load_builder_tools_config()
467
+ blocked: list[str] = []
468
+ has_project_config = False
469
+ if workdir:
470
+ if workdir in cfg:
471
+ blocked = cfg[workdir]
472
+ has_project_config = True
473
+ else:
474
+ blocked = cfg.get("_global_defaults", [])
475
+ global_blocked = cfg.get("_global_defaults", [])
476
+ body = json.dumps({"servers": servers, "blocked": blocked, "global_blocked": global_blocked, "has_project_config": has_project_config}).encode()
477
+ self._respond(200, "application/json", body)
478
+ elif self.path == "/api/global-mcp-defaults":
479
+ cfg = agent_runner.load_builder_tools_config()
480
+ body = json.dumps({"blocked": cfg.get("_global_defaults", [])}).encode()
481
+ self._respond(200, "application/json", body)
461
482
  elif self.path == "/api/headroom-stats":
462
483
  try:
463
484
  req = urllib.request.Request("http://127.0.0.1:8787/stats")
@@ -719,14 +740,16 @@ class _Handler(BaseHTTPRequestHandler):
719
740
  _LAST_WORKDIR_FILE.write_text(working_dir, encoding="utf-8")
720
741
  except OSError:
721
742
  pass
722
- auto_commit = data.get("auto_commit", True)
743
+ auto_commit = data.get("auto_commit", False)
723
744
  timeout_minutes = int(data.get("timeout_minutes", 60))
724
745
  manager_model = data.get("manager_model", "")
725
746
  builder_model = data.get("builder_model", "")
726
747
  reviewer_model = data.get("reviewer_model", "")
727
748
  user_name = str(data.get("user_name", "")).strip()
749
+ blocked_mcp = data.get("blocked_mcp_servers", [])
728
750
  result = agent_runner.start_run(task=task, working_dir=working_dir, context=context, auto_commit=auto_commit, timeout_minutes=timeout_minutes,
729
- manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name)
751
+ manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name,
752
+ blocked_mcp_servers=blocked_mcp)
730
753
  body = json.dumps(result).encode()
731
754
  status_code = 200 if "session_id" in result else 400
732
755
  self._respond(status_code, "application/json", body)
@@ -768,6 +791,49 @@ class _Handler(BaseHTTPRequestHandler):
768
791
  except Exception as exc:
769
792
  body = json.dumps({"success": False, "error": str(exc)}).encode()
770
793
  self._respond(500, "application/json", body)
794
+ elif self.path == "/api/builder-tools-config":
795
+ try:
796
+ content_len = int(self.headers.get("Content-Length", 0))
797
+ raw = self.rfile.read(content_len).decode("utf-8") if content_len else ""
798
+ data = json.loads(raw)
799
+ workdir = data.get("workdir", "").strip()
800
+ blocked = data.get("blocked", [])
801
+ if not workdir:
802
+ body = json.dumps({"success": False, "error": "Missing 'workdir' field."}).encode()
803
+ self._respond(400, "application/json", body)
804
+ return
805
+ cfg = agent_runner.load_builder_tools_config()
806
+ cfg[workdir] = blocked
807
+ agent_runner.save_builder_tools_config(cfg)
808
+ body = json.dumps({"success": True}).encode()
809
+ self._respond(200, "application/json", body)
810
+ except (json.JSONDecodeError, ValueError):
811
+ body = json.dumps({"success": False, "error": "Invalid JSON body."}).encode()
812
+ self._respond(400, "application/json", body)
813
+ except Exception as exc:
814
+ body = json.dumps({"success": False, "error": str(exc)}).encode()
815
+ self._respond(500, "application/json", body)
816
+ elif self.path == "/api/global-mcp-defaults":
817
+ try:
818
+ content_len = int(self.headers.get("Content-Length", 0))
819
+ raw = self.rfile.read(content_len).decode("utf-8") if content_len else ""
820
+ data = json.loads(raw)
821
+ blocked = data.get("blocked", [])
822
+ cfg = agent_runner.load_builder_tools_config()
823
+ cfg["_global_defaults"] = blocked
824
+ if data.get("apply_all"):
825
+ for key in list(cfg.keys()):
826
+ if key != "_global_defaults":
827
+ cfg[key] = list(blocked)
828
+ agent_runner.save_builder_tools_config(cfg)
829
+ body = json.dumps({"success": True}).encode()
830
+ self._respond(200, "application/json", body)
831
+ except (json.JSONDecodeError, ValueError):
832
+ body = json.dumps({"success": False, "error": "Invalid JSON body."}).encode()
833
+ self._respond(400, "application/json", body)
834
+ except Exception as exc:
835
+ body = json.dumps({"success": False, "error": str(exc)}).encode()
836
+ self._respond(500, "application/json", body)
771
837
  elif self.path == "/api/burn-report/reset":
772
838
  try:
773
839
  from .stats import reset_burn_stats
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.4
3
+ Version: 4.0.6
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0