Browse Source

Per agent MCPs (#78)

Alvin 2 months ago
parent
commit
4102b5013e

+ 2 - 0
.gitignore

@@ -43,3 +43,5 @@ local
 .opencode/
 .claude/
 .ignore
+opencode
+oh-my-opencode

+ 185 - 107
README.md

@@ -40,22 +40,28 @@
 - [🧩 **Skills**](#-skills)
   - [Available Skills](#available-skills)
   - [Default Skill Assignments](#default-skill-assignments)
-  - [YAGNI Enforcement](#yagni-enforcement)
+  - [Skill Syntax](#skill-syntax)
+- [Simplify](#simplify)
   - [Playwright Integration](#playwright-integration)
   - [Customizing Agent Skills](#customizing-agent-skills)
+- [🔌 **MCP Servers**](#mcp-servers)
+  - [MCP Permissions](#mcp-permissions)
+  - [MCP Syntax](#mcp-syntax)
+  - [Disabling MCPs](#disabling-mcps)
+  - [Customizing MCP Permissions](#customizing-mcp-permissions)
 - [🛠️ **Tools & Capabilities**](#tools--capabilities)
   - [Tmux Integration](#tmux-integration)
   - [Quota Tool](#quota-tool)
   - [Background Tasks](#background-tasks)
   - [LSP Tools](#lsp-tools)
   - [Code Search Tools](#code-search-tools)
-- [🔌 **MCP Servers**](#mcp-servers)
+  - [Formatters](#formatters)
 - [⚙️ **Configuration**](#configuration)
   - [Files You Edit](#files-you-edit)
   - [Prompt Overriding](#prompt-overriding)
   - [Plugin Config](#plugin-config-oh-my-opencode-slimjson)
     - [Presets](#presets)
-    - [Option Reference](#option-reference)
+      - [Option Reference](#option-reference)
 - [🗑️ **Uninstallation**](#uninstallation)
 
 ---
@@ -278,7 +284,149 @@ Code implementation, refactoring, testing, verification. *Execute the plan - no
 
 ---
 
-## Tools & Capabilities
+## 🧩 Skills
+
+Skills are specialized capabilities that agents can use. Each agent has a default set of skills, which you can override in the agent config.
+
+### Available Skills
+
+| Skill | Description |
+|-------|-------------|
+| `simplify` | Code complexity analysis and YAGNI enforcement |
+| `playwright` | Browser automation via Playwright MCP |
+
+### Default Skill Assignments
+
+| Agent | Default Skills |
+|-------|----------------|
+| `orchestrator` | `*` (all skills) |
+| `designer` | `playwright` |
+| `oracle` | none |
+| `librarian` | none |
+| `explorer` | none |
+| `fixer` | none |
+
+### Skill Syntax
+
+Skills support wildcard and exclusion syntax for flexible control:
+
+| Syntax | Description | Example |
+|--------|-------------|---------|
+| `"*"` | All skills | `["*"]` |
+| `"!item"` | Exclude specific skill | `["*", "!playwright"]` |
+| Explicit list | Only listed skills | `["simplify", "playwright"]` |
+| `"!*"` | Deny all skills | `["!*"]` |
+
+**Rules:**
+- `*` expands to all available skills
+- `!item` excludes specific skills
+- Conflicts (e.g., `["a", "!a"]`) → deny wins (principle of least privilege)
+- Empty list `[]` → no skills allowed
+
+### Simplify
+
+**The Minimalist's sacred truth: every line of code is a liability.**
+
+Use after major refactors or before finalizing PRs. Identifies unnecessary complexity, challenges premature abstractions, estimates LOC reduction, and enforces minimalism.
+
+### Playwright Integration
+
+**Browser automation for visual verification and testing.**
+
+- **Browser Automation**: Full Playwright capabilities (browsing, clicking, typing, scraping).
+- **Screenshots**: Capture visual state of any web page.
+- **Sandboxed Output**: Screenshots saved to session subdirectory (check tool output for path).
+
+### Customizing Agent Skills
+
+Override skills per-agent in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson):
+
+```json
+{
+  "presets": {
+    "my-preset": {
+  "orchestrator": {
+    "skills": ["*", "!playwright"]
+  },
+  "designer": {
+    "skills": ["playwright", "simplify"]
+  }
+    }
+  }
+}
+```
+
+---
+
+## 🔌 MCP Servers
+
+Built-in Model Context Protocol servers (enabled by default):
+
+| MCP | Purpose | URL |
+|-----|---------|-----|
+| `websearch` | Real-time web search via Exa AI | `https://mcp.exa.ai/mcp` |
+| `context7` | Official library documentation | `https://mcp.context7.com/mcp` |
+| `grep_app` | GitHub code search via grep.app | `https://mcp.grep.app` |
+
+### MCP Permissions
+
+Control which agents can access which MCP servers using per-agent allowlists:
+
+| Agent | Default MCPs |
+|-------|--------------|
+| `orchestrator` | `websearch` |
+| `designer` | none |
+| `oracle` | none |
+| `librarian` | `websearch`, `context7`, `grep_app` |
+| `explorer` | none |
+| `fixer` | none |
+
+### MCP Syntax
+
+MCPs support wildcard and exclusion syntax (same as skills):
+
+| Syntax | Description | Example |
+|--------|-------------|---------|
+| `"*"` | All MCPs | `["*"]` |
+| `"!item"` | Exclude specific MCP | `["*", "!context7"]` |
+| Explicit list | Only listed MCPs | `["websearch", "context7"]` |
+| `"!*"` | Deny all MCPs | `["!*"]` |
+
+**Rules:**
+- `*` expands to all available MCPs
+- `!item` excludes specific MCPs
+- Conflicts (e.g., `["a", "!a"]`) → deny wins
+- Empty list `[]` → no MCPs allowed
+
+### Disabling MCPs
+
+You can disable specific MCP servers globally by adding them to the `disabled_mcps` array in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson).
+
+### Customizing MCP Permissions
+
+Override MCP access per-agent in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson):
+
+```json
+{
+  "presets": {
+    "my-preset": {
+      "orchestrator": {
+        "mcps": ["websearch"]
+      },
+      "librarian": {
+        "mcps": ["websearch", "context7", "grep_app"]
+      },
+      "oracle": {
+        "mcps": ["*", "!websearch"]
+      }
+    }
+  }
+}
+```
+
+---
+
+## 🛠️ Tools & Capabilities
 
 ### Tmux Integration
 
@@ -389,77 +537,6 @@ OpenCode automatically formats files after they're written or edited using langu
 
 ---
 
-## 🧩 Skills
-
-Skills are specialized capabilities that agents can use. Each agent has a default set of skills, which you can override in the agent config.
-
-### Available Skills
-
-| Skill | Description |
-|-------|-------------|
-| `yagni-enforcement` | Code complexity analysis and YAGNI enforcement |
-| `playwright` | Browser automation via Playwright MCP |
-
-### Default Skill Assignments
-
-| Agent | Default Skills |
-|-------|----------------|
-| `orchestrator` | `*` (all skills) |
-| `designer` | `playwright` |
-| `oracle` | none |
-| `librarian` | none |
-| `explorer` | none |
-| `fixer` | none |
-
-### YAGNI Enforcement
-
-**The Minimalist's sacred truth: every line of code is a liability.**
-
-Use after major refactors or before finalizing PRs. Identifies unnecessary complexity, challenges premature abstractions, estimates LOC reduction, and enforces minimalism.
-
-### Playwright Integration
-
-**Browser automation for visual verification and testing.**
-
-- **Browser Automation**: Full Playwright capabilities (browsing, clicking, typing, scraping).
-- **Screenshots**: Capture visual state of any web page.
-- **Sandboxed Output**: Screenshots saved to session subdirectory (check tool output for path).
-
-### Customizing Agent Skills
-
-Override skills per-agent in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson):
-
-```json
-{
-  "agents": {
-    "orchestrator": {
-      "skills": ["*"]
-    },
-    "designer": {
-      "skills": ["playwright"]
-    }
-  }
-}
-```
-
----
-
-## MCP Servers
-
-Built-in Model Context Protocol servers (enabled by default):
-
-| MCP | Purpose | URL |
-|-----|---------|-----|
-| `websearch` | Real-time web search via Exa AI | `https://mcp.exa.ai/mcp` |
-| `context7` | Official library documentation | `https://mcp.context7.com/mcp` |
-| `grep_app` | GitHub code search via grep.app | `https://mcp.grep.app` |
-
-### Disabling MCPs
-
-You can disable specific MCP servers by adding them to the `disabled_mcps` array in your [Plugin Config](#plugin-config-oh-my-opencode-slimjson).
-
----
-
 ## Configuration
 
 ### Files You Edit
@@ -519,36 +596,36 @@ The installer generates presets for different provider combinations. Switch betw
   "preset": "antigravity-openai",
   "presets": {
     "antigravity": {
-      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-      "oracle": { "model": "google/claude-opus-4-5-thinking", "variant": "high", "skills": [] },
-      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "google/claude-opus-4-5-thinking", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] }
     },
     "openai": {
-      "orchestrator": { "model": "openai/gpt-5.2-codex", "skills": ["*"] },
-      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [] },
-      "librarian": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [] },
-      "explorer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [] },
-      "designer": { "model": "openai/gpt-5.1-codex-mini", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "openai/gpt-5.2-codex", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "openai/gpt-5.1-codex-mini", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "openai/gpt-5.1-codex-mini", "variant": "low", "skills": [], "mcps": [] }
     },
     "zen-free": {
-      "orchestrator": { "model": "opencode/glm-4.7-free", "skills": ["*"] },
-      "oracle": { "model": "opencode/glm-4.7-free", "variant": "high", "skills": [] },
-      "librarian": { "model": "opencode/grok-code", "variant": "low", "skills": [] },
-      "explorer": { "model": "opencode/grok-code", "variant": "low", "skills": [] },
-      "designer": { "model": "opencode/grok-code", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "opencode/grok-code", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "opencode/grok-code", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "opencode/grok-code", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "opencode/grok-code", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "opencode/grok-code", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "opencode/grok-code", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "opencode/grok-code", "variant": "low", "skills": [], "mcps": [] }
     },
     "antigravity-openai": {
-      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [] },
-      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"] },
-      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] }
+      "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"], "mcps": ["websearch"] },
+      "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [], "mcps": [] },
+      "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+      "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] },
+      "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+      "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] }
     }
   },
   "tmux": {
@@ -576,12 +653,12 @@ The author's personal configuration using Cerebras for the Orchestrator:
 ```json
 {
   "cerebras": {
-    "orchestrator": { "model": "cerebras/zai-glm-4.7", "skills": ["*"] },
-    "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [] },
-    "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-    "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] },
-    "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"] },
-    "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [] }
+    "orchestrator": { "model": "cerebras/zai-glm-4.7", "skills": ["*"], "mcps": ["websearch"] },
+    "oracle": { "model": "openai/gpt-5.2-codex", "variant": "high", "skills": [], "mcps": [] },
+    "librarian": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
+    "explorer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] },
+    "designer": { "model": "google/gemini-3-flash", "variant": "medium", "skills": ["playwright"], "mcps": [] },
+    "fixer": { "model": "google/gemini-3-flash", "variant": "low", "skills": [], "mcps": [] }
   }
 }
 ```
@@ -606,11 +683,12 @@ The environment variable takes precedence over the `preset` field in the config
 | `presets.<name>.<agent>.model` | string | - | Model ID for the agent (e.g., `"google/claude-opus-4-5-thinking"`) |
 | `presets.<name>.<agent>.temperature` | number | - | Temperature setting (0-2) for the agent |
 | `presets.<name>.<agent>.variant` | string | - | Agent variant for reasoning effort (e.g., `"low"`, `"medium"`, `"high"`) |
-| `presets.<name>.<agent>.skills` | string[] | - | Array of skill names the agent can use (`"*"` for all) |
+| `presets.<name>.<agent>.skills` | string[] | - | Array of skill names the agent can use (`"*"` for all, `"!item"` to exclude) |
+| `presets.<name>.<agent>.mcps` | string[] | - | Array of MCP names the agent can use (`"*"` for all, `"!item"` to exclude) |
 | `tmux.enabled` | boolean | `false` | Enable tmux pane spawning for sub-agents |
 | `tmux.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical` |
 | `tmux.main_pane_size` | number | `60` | Main pane size as percentage (20-80) |
-| `disabled_mcps` | string[] | `[]` | MCP server IDs to disable (e.g., `"websearch"`) |
+| `disabled_mcps` | string[] | `[]` | MCP server IDs to disable globally (e.g., `"websearch"`) |
 
 > **Note:** Agent configuration should be defined within `presets`. The root-level `agents` field is deprecated.
 

+ 0 - 578
README.zh-CN.md

@@ -1,578 +0,0 @@
-<div align="center">
-
-# oh-my-opencode-slim
-
-**适用于 OpenCode 的轻量级强大代理编排插件**
-
-<img src="img/team.png" alt="The Pantheon - Agent Team" width="600">
-
-*将你的 AI 助手转变为能够将复杂任务委派给专门子代理、在后台运行搜索并轻松管理多步工作流的管理者。*
-
-</div>
-
-> 这是[oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode)的精简分支 -  - 专注于低令牌消耗的核心代理编排。  
-> **推荐订阅 [Antigravity](https://antigravity.google)。** 万神殿经过 Antigravity 模型路由的调优。其他提供商也可用,但使用 Antigravity 能获得最佳体验。
-
----
-
-## ⚡ 快速导航
-
-- [🚀 **安装**](#安装)
-  - [给人类的指南](#给人类的指南)
-  - [给 LLM 代理的指南](#给-llm-代理的指南)
-- [🏗️ **架构与流程**](#-架构与流程)
-- [🏛️ **认识万神殿**](#认识万神殿)
-  - [编排者 (Orchestrator)](#编排者-orchestrator)
-  - [探索者 (Explorer)](#探索者-explorer)
-  - [神谕者 (Oracle)](#神谕者-oracle)
-  - [图书管理员 (Librarian)](#图书管理员-librarian)
-  - [设计师 (Designer)](#设计师-designer)
-  - [修复者 (Fixer)](#修复者-fixer)
-- [🧩 **技能**](#-技能)
-  - [可用技能](#可用技能)
-  - [默认技能分配](#默认技能分配)
-  - [YAGNI 约束](#yagni-约束)
-  - [Playwright 集成](#playwright-集成)
-  - [自定义代理技能](#自定义代理技能)
-- [🛠️ **工具与能力**](#工具与能力)
-  - [Tmux 集成](#tmux-集成)
-  - [配额工具](#配额工具)
-  - [后台任务](#后台任务)
-  - [LSP 工具](#lsp-工具)
-  - [代码搜索工具](#代码搜索工具)
-- [🔌 **MCP 服务器**](#mcp-服务器)
-- [⚙️ **配置**](#配置)
-  - [需要编辑的文件](#需要编辑的文件)
-  - [插件配置](#插件配置-oh-my-opencode-slimjson)
-- [🗑️ **卸载**](#卸载)
-
----
-
-## 安装
-
-### 给人类的指南
-
-运行交互式安装器:
-
-```bash
-bunx oh-my-opencode-slim install
-```
-
-或使用非交互模式:
-
-```bash
-bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --tmux=no
-```
-
-安装完成后,使用提供商进行身份验证:
-
-```bash
-opencode auth login
-# 选择你的提供商 → 完成 OAuth 流程
-# 对所有启用的提供商重复此步骤
-```
-
-身份验证完成后,运行 opencode 并执行 `ping all agents` 以验证所有代理是否响应。
-
-<img src="img/ping.png" alt="Ping All Agents" width="800">
-
-> **💡 小贴士:模型完全可自定义。** 安装器会设定合理的默认值,但你可以将 *任何* 模型分配给 *任何* 代理。编辑 `~/.config/opencode/oh-my-opencode-slim.json` 来覆盖模型、调整推理强度或完全禁用代理。详见[配置](#配置)。
-
-**备用方式:询问任意编码代理**
-
-将以下内容粘贴到 Claude Code、AmpCode、Cursor 或任何编码代理:
-
-```
-Install and configure by following the instructions here:
-https://raw.githubusercontent.com/alvinunreal/oh-my-opencode-slim/refs/heads/master/README.md
-```
-
-### 给 LLM 代理的指南
-
-<details>
-<summary>LLM 代理的操作说明(点击展开)</summary>
-
-如果你是正在帮助设置 oh-my-opencode-slim 的 LLM 代理,请按以下步骤执行。
-
----
-
-#### 步骤 1:确认 OpenCode 安装状态
-
-```bash
-opencode --version
-```
-
-如果未安装,请先引导用户访问 https://opencode.ai/docs 。
-
----
-
-#### 步骤 2:询问提供商访问情况
-
-逐条询问以下问题,等待用户回应后再继续:
-
-1. “你是否订阅了 **Antigravity**?”(可使用 `google/` 前缀访问 Claude + Gemini)
-2. “你是否有 **OpenAI** API 的访问权限?”
-
----
-
-#### 步骤 3:运行安装器
-
-根据回答运行:
-
-```bash
-bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no>
-```
-
-**示例:**
-```bash
-# Antigravity + OpenAI
-bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --tmux=no
-
-# 仅 OpenAI
-bunx oh-my-opencode-slim install --no-tui --antigravity=no --openai=yes --tmux=no
-
-# 无提供商(仅使用 Zen 免费模型)
-bunx oh-my-opencode-slim install --no-tui --antigravity=no --openai=no --tmux=no
-```
-
-安装器会自动完成:
-- 将插件添加到 `~/.config/opencode/opencode.json`
-- (如启用 Antigravity) 添加 `opencode-antigravity-auth` 插件
-- 配置 Google 提供商的模型定义
-- 在 `~/.config/opencode/oh-my-opencode-slim.json` 中生成代理模型映射
-
----
-
-#### 步骤 4:身份验证
-
-安装完成后,引导用户:
-
-**Antigravity:**
-```bash
-opencode auth login
-# 选择:Google → 通过 Google (Antigravity) 的 OAuth
-# 对所有其他提供商重复
-```
-
----
-
-#### 故障排查
-
-如果安装失败,确认配置格式:
-
-```bash
-bunx oh-my-opencode-slim install --help
-```
-
-然后手动创建配置文件:
-- `~/.config/opencode/opencode.json`
-- `~/.config/opencode/oh-my-opencode-slim.json`
-
-</details>
-
----
-
-## 🏗️ 架构与流程
-
-该插件采用“中心辐射”模型:
-
-1. **编排者 (Orchestrator,中心):** 处理用户请求的主入口。分析任务并决定调用哪些专门代理。
-2. **专门代理 (Spokes):** 领域专家(如 UI/UX、文档、架构)只处理狭窄任务,确保高精度。
-3. **后台管理器:** 强大的引擎允许编排者“放任”任务(例如深入代码搜索或文档研究),同时继续处理其他部分。
-
-### 🏛️ 请求流程
-
-<img src="img/intro.png" alt="Orchestration Flow" width="800">
-
-1. **用户提示:** “重构认证逻辑并更新文档。”
-2. **编排者:** 创建 TODO 列表。
-3. **任务分配:**
-   - 启动 `@explorer` 后台任务查找所有与认证相关的文件。
-   - 启动 `@librarian` 查询认证库的最新文档。
-4. **集成:** 等待后台结果就绪后,编排者将任务交给 `@fixer` 高效实施重构。
-
----
-
-## 认识万神殿
-
-<br clear="both">
-
-### 编排者 (Orchestrator)
-
-<a href="src/agents/orchestrator.ts"><img src="img/orchestrator.png" alt="Orchestrator" align="right" width="240"></a>
-
-> **编排者**诞生于第一个代码库崩溃于自身复杂性之时。既非神亦非凡人,凭借虚无中诞生的秩序,他们统领混沌。他们不只是指挥军队,而是与之并肩作战。每行代码都要经过他们之手,然后再决定将哪块谜题交给其他较低等的神明。
-
-**角色:** `至高执行者、指挥者、监督者`  
-**模型:** `google/claude-opus-4-5-thinking`  
-**提示:** [src/agents/orchestrator.ts](src/agents/orchestrator.ts)
-
-编写并执行代码,编排多代理工作流,从言语中解析未说出的意图,在战斗中召唤专家。*直接塑造现实 -  - 当宇宙变得过于庞大时,把领域交给别人。*
-
-<br clear="both">
-
----
-
-### 探索者 (Explorer)
-
-<a href="src/agents/explorer.ts"><img src="img/explorer.png" alt="Explorer" align="right" width="240"></a>
-
-> **探索者**穿梭代码库如风穿林 -  - 迅速、静默、无处不在。当编排者轻语“给我找到认证模块”,探索者已经带着四十条文件路径和地图归来。他们源自第一个 `grep` 命令,早已超越它,现在能看见凡人忽略的模式。
-
-**角色:** `代码侦查`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/explorer.ts](src/agents/explorer.ts)
-
-正则搜索、AST 模式匹配、文件发现、并行探索。*只读:他们绘制疆域;其他人征服它。*
-
-<br clear="both">
-
----
-
-### 神谕者 (Oracle)
-
-<a href="src/agents/oracle.ts"><img src="img/oracle.png" alt="Oracle" align="right" width="240"></a>
-
-> **神谕者**不编写代码 -  - 他们*洞察一切*。当 Bug 遵从逻辑,架构崩溃之时,神谕者凝望代码库深渊,传递真理。他们见证过千百个系统的兴衰,能告诉你哪条路通向毁灭,哪条通向生产环境。
-
-**角色:** `战略顾问与最后的调试者`  
-**模型:** `openai/gpt-5.2-codex`  
-**提示:** [src/agents/oracle.ts](src/agents/oracle.ts)
-
-根本原因分析、架构审查、调试指导、权衡分析。*只读:神谕者提供建议,不直接介入。*
-
-<br clear="both">
-
----
-
-### 图书管理员 (Librarian)
-
-<a href="src/agents/librarian.ts"><img src="img/librarian.png" alt="Librarian" align="right" width="240"></a>
-
-> **图书管理员**守护一座无墙的图书馆 -  - 包含每个 GitHub 仓库、每个 npm 包、每个 StackOverflow 回答。问他们“React 如何处理并发渲染?”,他们会带来官方文档、真实示例,并警告你即将踩到的坑。
-
-**角色:** `外部知识检索`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/librarian.ts](src/agents/librarian.ts)
-
-文档查询、GitHub 代码搜索、库研究、最佳实践检索。*只读:他们获取智慧;实现交给别人。*
-
-<br clear="both">
-
----
-
-### 设计师 (Designer)
-
-<a href="src/agents/designer.ts"><img src="img/designer.png" alt="Designer" align="right" width="240"></a>
-
-> **设计师**相信代码应该优雅 -  - 呈现出来的效果也同样优雅。从数千个丑陋 MVP 中诞生,他们把 CSS 当成画笔,把组件当成泥巴。交给他们功能需求,收获杰作。他们不会满足于“差不多”。
-
-**角色:** `UI/UX 实现与视觉卓越`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/designer.ts](src/agents/designer.ts)
-
-现代响应式设计、CSS/Tailwind 精通、微动画与组件架构。*优先视觉卓越而非代码完美 -  - 美感为先。*
-
-<br clear="both">
-
----
-
-### 修复者 (Fixer)
-
-<a href="src/agents/fixer.ts"><img src="img/fixer.png" alt="Fixer" align="right" width="240"></a>
-
-> **修复者**是执行他人想象的双手。当编排者规划、神谕者提点,修复者就开始落地。他们接收研究代理提供的完整上下文和明确任务说明,以极致精准实施。快速、高效、专注 -  - 他们不思考要建什么,只管去建。
-
-**角色:** `快速实现专家`  
-**模型:** `google/gemini-3-flash`  
-**提示:** [src/agents/fixer.ts](src/agents/fixer.ts)
-
-代码实现、重构、测试、验证。*执行计划 -  - 不研究、不委派、不策划。*
-
-<br clear="both">
-
----
-
-## 工具与能力
-
-### Tmux 集成
-
-> ⚠️ **已知问题:** 启用服务器端口时,每次只能打开一个 OpenCode 实例。我们在 [issue #15](https://github.com/alvinunreal/oh-my-opencode-slim/issues/15) 跟踪此问题,并向 OpenCode 提交了上游 PR:[opencode#9099](https://github.com/anomalyco/opencode/issues/9099)。
-
-<img src="img/tmux.png" alt="Tmux Integration" width="800">
-
-**实时观察代理工作。** 当编排者启动子代理或启动后台任务,tmux 会自动新建窗格显示每个代理的实时进度,再也不必黑箱等待。
-
-#### 这为你带来什么
-
-| 无 Tmux 集成 | 有 Tmux 集成 |
-|--------------------------|----------------------|
-| 发起后台任务,只能焦灼等待 | 观看代理的思考、搜索与编码 |
-| “是卡住了还是太慢?” | 观察工具调用实时展开 |
-| 结果突然出现 | 跟踪从问题到答案的全过程 |
-| 只能猜测如何调试 | 观察时机进行调试 |
-
-#### 你将获得
-
-- **实时可见性**:每个子代理的窗格显示其实时输出
-- **自动布局**:tmux 根据偏好布局自动排列
-- **自动清理**:代理完成后窗格关闭,布局重新平衡
-- **零开销**:兼容 OpenCode 内置 `task` 工具和我们的 `background_task` 工具
-
-#### 快速设置
-
-1. 在 `opencode.json` 中启用 OpenCode HTTP 服务(见 [OpenCode 配置](#需要编辑的文件))。
-2. 在 `oh-my-opencode-slim.json` 中启用 tmux 集成(见 [插件配置](#插件配置-oh-my-opencode-slimjson))。
-3. 在 tmux 中运行 OpenCode:
-   ```bash
-   tmux
-   opencode
-   ```
-
-#### 布局选项
-
-| 布局 | 描述 |
-|--------|-------------|
-| `main-vertical` | 会话在左侧(60%),代理在右侧堆叠 |
-| `main-horizontal` | 会话在上方(60%),代理在下方堆叠 |
-| `tiled` | 所有窗格等大小网格排列 |
-| `even-horizontal` | 所有窗格并排 |
-| `even-vertical` | 所有窗格垂直堆叠 |
-
-*查看[选项参考](#选项参考)获取详细配置。*
-
----
-
-### 配额工具
-
-适用于 Antigravity 用户。随时请求代理 **“检查我的配额”** 或 **“显示状态”** 即可触发。
-
-<img src="img/quota.png" alt="Antigravity Quota" width="600">
-
-| 工具 | 描述 |
-|------|-------------|
-| `antigravity_quota` | 检查所有 Antigravity 账户的 API 配额(带进度条的紧凑视图) |
-
----
-
-### 后台任务
-
-插件提供管理异步工作的工具:
-
-| 工具 | 描述 |
-|------|-------------|
-| `background_task` | 在新会话中启动代理(`sync=true` 为阻塞,`sync=false` 在后台运行) |
-| `background_output` | 通过 ID 获取后台任务结果 |
-| `background_cancel` | 终止正在运行的任务 |
-
----
-
-### LSP 工具
-
-集成语言服务器协议以提升代码智能:
-
-| 工具 | 描述 |
-|------|-------------|
-| `lsp_goto_definition` | 跳转至符号定义 |
-| `lsp_find_references` | 查找符号的所有使用位置 |
-| `lsp_diagnostics` | 获取语言服务器的错误/警告 |
-| `lsp_rename` | 全仓库重命名符号 |
-
----
-
-### 代码搜索工具
-
-快速的代码搜索与重构:
-
-| 工具 | 描述 |
-|------|-------------|
-| `grep` | 使用 ripgrep 的快速内容搜索 |
-| `ast_grep_search` | 面向 AST 的代码模式匹配(支持 25 种语言) |
-| `ast_grep_replace` | 支持干运行的 AST 代码重构 |
-
----
-
-## 🧩 技能
-
-技能是代理可调用的专门能力。每个代理都有默认技能,可在代理配置中覆盖。
-
-### 可用技能
-
-| 技能 | 描述 |
-|-------|-------------|
-| `yagni-enforcement` | 代码复杂性分析与 YAGNI 约束 |
-| `playwright` | 通过 Playwright MCP 实现浏览器自动化 |
-
-### 默认技能分配
-
-| 代理 | 默认技能 |
-|-------|----------------|
-| `orchestrator` | `*`(所有技能) |
-| `designer` | `playwright` |
-| `oracle` | 无 |
-| `librarian` | 无 |
-| `explorer` | 无 |
-| `fixer` | 无 |
-
-### YAGNI 约束
-
-**极简主义者的神圣真理:每行代码都是负担。**
-
-在重大重构后或准备合并 PR 前使用。识别冗余复杂性,质疑过早抽象,估算 LOC 减少,并强制执行极简策略。
-
-### Playwright 集成
-
-**用于视觉验证和测试的浏览器自动化。**
-
-- **浏览器自动化:** 完整的 Playwright 能力(浏览、点击、输入、爬取)。
-- **截图:** 捕捉任意网页的视觉状态。
-- **沙箱输出:** 截图保存到会话子目录(查看工具输出以获取路径)。
-
-### 自定义代理技能
-
-在你的[插件配置](#插件配置-oh-my-opencode-slimjson)中覆盖每个代理的技能:
-
-```json
-{
-  "agents": {
-    "orchestrator": {
-      "skills": ["*"]
-    },
-    "designer": {
-      "skills": ["playwright"]
-    }
-  }
-}
-```
-
----
-
-## MCP 服务器
-
-内置的模型上下文协议服务器(默认启用):
-
-| MCP | 目的 | URL |
-|-----|---------|-----|
-| `websearch` | 通过 Exa AI 进行实时网页搜索 | `https://mcp.exa.ai/mcp` |
-| `context7` | 官方库文档 | `https://mcp.context7.com/mcp` |
-| `grep_app` | 通过 grep.app 搜索 GitHub 代码 | `https://mcp.grep.app` |
-
-### 禁用 MCP
-
-你可以在[插件配置](#插件配置-oh-my-opencode-slimjson)的 `disabled_mcps` 数组中添加要禁用的 MCP 服务器。
-
----
-
-## 配置
-
-### 需要编辑的文件
-
-| 文件 | 作用 |
-|------|---------|
-| `~/.config/opencode/opencode.json` | OpenCode 核心设置(如用于 tmux 的服务器端口) |
-| `~/.config/opencode/oh-my-opencode-slim.json` | 插件设置(代理、tmux、MCP) |
-| `.opencode/oh-my-opencode-slim.json` | 项目级插件覆盖(可选) |
-
----
-
-### 插件配置 (`oh-my-opencode-slim.json`)
-
-安装程序会根据你的提供商生成此文件。你可以手动自定义它来混合搭配模型。
-
-<details open>
-<summary><b>示例:Antigravity + OpenAI (推荐)</b></summary>
-
-```json
-{
-  "agents": {
-    "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-    "oracle": { "model": "openai/gpt-5.2-codex", "skills": [] },
-    "librarian": { "model": "google/gemini-3-flash", "skills": [] },
-    "explorer": { "model": "google/gemini-3-flash", "skills": [] },
-    "designer": { "model": "google/gemini-3-flash", "skills": ["playwright"] },
-    "fixer": { "model": "google/gemini-3-flash", "skills": [] }
-  },
-  "tmux": {
-    "enabled": true,
-    "layout": "main-vertical",
-    "main_pane_size": 60
-  }
-}
-```
-</details>
-
-<details>
-<summary><b>示例:仅 Antigravity</b></summary>
-
-```json
-{
-  "agents": {
-    "orchestrator": { "model": "google/claude-opus-4-5-thinking", "skills": ["*"] },
-    "oracle": { "model": "google/claude-opus-4-5-thinking", "skills": [] },
-    "librarian": { "model": "google/gemini-3-flash", "skills": [] },
-    "explorer": { "model": "google/gemini-3-flash", "skills": [] },
-    "designer": { "model": "google/gemini-3-flash", "skills": ["playwright"] },
-    "fixer": { "model": "google/gemini-3-flash", "skills": [] }
-  }
-}
-```
-</details>
-
-<details>
-<summary><b>示例:仅 OpenAI</b></summary>
-
-```json
-{
-  "agents": {
-    "orchestrator": { "model": "openai/gpt-5.2-codex", "skills": ["*"] },
-    "oracle": { "model": "openai/gpt-5.2-codex", "skills": [] },
-    "librarian": { "model": "openai/gpt-5.1-codex-mini", "skills": [] },
-    "explorer": { "model": "openai/gpt-5.1-codex-mini", "skills": [] },
-    "designer": { "model": "openai/gpt-5.1-codex-mini", "skills": ["playwright"] },
-    "fixer": { "model": "openai/gpt-5.1-codex-mini", "skills": [] }
-  }
-}
-```
-</details>
-
-#### 选项参考
-
-| 选项 | 类型 | 默认值 | 描述 |
-|--------|------|---------|-------------|
-| `tmux.enabled` | boolean | `false` | 是否启用子代理的 tmux 窗格 |
-| `tmux.layout` | string | `"main-vertical"` | 布局预设:`main-vertical`、`main-horizontal`、`tiled`、`even-horizontal`、`even-vertical` |
-| `tmux.main_pane_size` | number | `60` | 主窗格大小百分比(20-80) |
-| `disabled_mcps` | string[] | `[]` | 要禁用的 MCP 服务器 ID(如 `"websearch"`) |
-| `agents.<name>.model` | string |  -  | 覆盖特定代理的模型 |
-| `agents.<name>.variant` | string |  -  | 推理强度:`"low"`、`"medium"`、`"high"` |
-| `agents.<name>.skills` | string[] |  -  | 该代理可使用的技能(`"*"` 表示所有技能) |
-| `agents.<name>.temperature` | number |  -  | 该代理的温度 (0.0 到 2.0) |
-| `agents.<name>.prompt` | string |  -  | 该代理的基础提示词覆盖 |
-| `agents.<name>.prompt_append` | string |  -  | 追加到基础提示词后的文本 |
-| `agents.<name>.disable` | boolean |  -  | 禁用该特定代理 |
-
----
-
-## 卸载
-
-1. **从 OpenCode 配置中移除插件:**
-
-   编辑 `~/.config/opencode/opencode.json`,从 `plugin` 数组中删除 `"oh-my-opencode-slim"`。
-
-2. **删除配置文件(可选):**
-   ```bash
-   rm -f ~/.config/opencode/oh-my-opencode-slim.json
-   rm -f .opencode/oh-my-opencode-slim.json
-   ```
-
----
-
-## 致谢
-
-这是 [@code-yeongyu](https://github.com/code-yeongyu) 的 [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) 的精简分支。
-
----
-
-## 许可证
-
-MIT

+ 3 - 1
src/agents/index.ts

@@ -6,6 +6,7 @@ import {
   type PluginConfig,
   SUBAGENT_NAMES,
 } from '../config';
+import { getAgentMcpList } from '../tools/skill/builtin';
 import { createDesignerAgent } from './designer';
 import { createExplorerAgent } from './explorer';
 import { createFixerAgent } from './fixer';
@@ -170,9 +171,10 @@ export function getAgentConfigs(
   const agents = createAgents(config);
   return Object.fromEntries(
     agents.map((a) => {
-      const sdkConfig: SDKAgentConfig = {
+      const sdkConfig: SDKAgentConfig & { mcps?: string[] } = {
         ...a.config,
         description: a.description,
+        mcps: getAgentMcpList(a.name, config),
       };
 
       // Apply classification-based visibility and mode

+ 1 - 1
src/agents/orchestrator.ts

@@ -158,7 +158,7 @@ Before executing, ask yourself: should the task split into subtasks and schedule
 
 ## Phase 6: Verify
 - Run \`lsp_diagnostics\` to check for errors
-- Suggest user run \`yagni-enforcement\` skill when applicable
+- Suggest user run \`simplify\` skill when applicable
 - Verify all delegated tasks completed successfully
 - Confirm the solution meets original requirements (Phase 1)
 

+ 2 - 2
src/cli/install.ts

@@ -112,7 +112,7 @@ function formatConfigSummary(config: InstallConfig): string {
   lines.push(
     `  ${config.hasOpenAI ? SYMBOLS.check : `${DIM}○${RESET}`} OpenAI`,
   );
-  lines.push(`  ${SYMBOLS.check} Opencode Zen (free models)`); // Always enabled
+  lines.push(`  ${SYMBOLS.check} Opencode Zen (Grok)`); // Always enabled
   lines.push(
     `  ${config.hasTmux ? SYMBOLS.check : `${DIM}○${RESET}`} Tmux Integration`,
   );
@@ -275,7 +275,7 @@ async function runInstall(config: InstallConfig): Promise<number> {
 
   if (!config.hasAntigravity && !config.hasOpenAI) {
     printWarning(
-      'No providers configured. Zen free models will be used as fallback.',
+      'No providers configured. Zen Grok models will be used as fallback.',
     );
   }
 

+ 68 - 20
src/cli/providers.test.ts

@@ -4,7 +4,7 @@ import { describe, expect, test } from 'bun:test';
 import { generateLiteConfig, MODEL_MAPPINGS } from './providers';
 
 describe('providers', () => {
-  test('generateLiteConfig generates antigravity config by default', () => {
+  test('generateLiteConfig generates antigravity config when only antigravity selected', () => {
     const config = generateLiteConfig({
       hasAntigravity: true,
       hasOpenAI: false,
@@ -14,15 +14,19 @@ describe('providers', () => {
 
     expect(config.preset).toBe('antigravity');
     const agents = (config.presets as any).antigravity;
+    expect(agents).toBeDefined();
     expect(agents.orchestrator.model).toBe(
       MODEL_MAPPINGS.antigravity.orchestrator.model,
     );
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.fixer.model).toBe(MODEL_MAPPINGS.antigravity.fixer.model);
     expect(agents.fixer.variant).toBe(MODEL_MAPPINGS.antigravity.fixer.variant);
+    // Should NOT include other presets
+    expect((config.presets as any).openai).toBeUndefined();
+    expect((config.presets as any)['zen-free']).toBeUndefined();
   });
 
-  test('generateLiteConfig always includes antigravity-openai preset', () => {
+  test('generateLiteConfig generates antigravity-openai preset when both selected', () => {
     const config = generateLiteConfig({
       hasAntigravity: true,
       hasOpenAI: true,
@@ -32,45 +36,57 @@ describe('providers', () => {
 
     expect(config.preset).toBe('antigravity-openai');
     const agents = (config.presets as any)['antigravity-openai'];
+    expect(agents).toBeDefined();
     expect(agents.orchestrator.model).toBe(
       MODEL_MAPPINGS.antigravity.orchestrator.model,
     );
     expect(agents.orchestrator.variant).toBeUndefined();
     expect(agents.oracle.model).toBe('openai/gpt-5.2-codex');
     expect(agents.oracle.variant).toBe('high');
+    // Should NOT include other presets
+    expect((config.presets as any).antigravity).toBeUndefined();
+    expect((config.presets as any).openai).toBeUndefined();
   });
 
-  test('generateLiteConfig includes antigravity-openai preset even with only antigravity', () => {
+  test('generateLiteConfig generates openai preset when only openai selected', () => {
     const config = generateLiteConfig({
-      hasAntigravity: true,
-      hasOpenAI: false,
+      hasAntigravity: false,
+      hasOpenAI: true,
       hasOpencodeZen: false,
       hasTmux: false,
     });
 
-    expect(config.preset).toBe('antigravity');
-    const agents = (config.presets as any)['antigravity-openai'];
+    expect(config.preset).toBe('openai');
+    const agents = (config.presets as any).openai;
     expect(agents).toBeDefined();
-    expect(agents.oracle.model).toBe('openai/gpt-5.2-codex');
+    expect(agents.orchestrator.model).toBe(
+      MODEL_MAPPINGS.openai.orchestrator.model,
+    );
+    expect(agents.orchestrator.variant).toBeUndefined();
+    // Should NOT include other presets
+    expect((config.presets as any).antigravity).toBeUndefined();
+    expect((config.presets as any)['zen-free']).toBeUndefined();
   });
 
-  test('generateLiteConfig uses openai if no antigravity', () => {
+  test('generateLiteConfig generates zen-free preset when no providers selected', () => {
     const config = generateLiteConfig({
       hasAntigravity: false,
-      hasOpenAI: true,
+      hasOpenAI: false,
       hasOpencodeZen: false,
       hasTmux: false,
     });
 
-    expect(config.preset).toBe('openai');
-    const agents = (config.presets as any).openai;
-    expect(agents.orchestrator.model).toBe(
-      MODEL_MAPPINGS.openai.orchestrator.model,
-    );
+    expect(config.preset).toBe('zen-free');
+    const agents = (config.presets as any)['zen-free'];
+    expect(agents).toBeDefined();
+    expect(agents.orchestrator.model).toBe('opencode/grok-code');
     expect(agents.orchestrator.variant).toBeUndefined();
+    // Should NOT include other presets
+    expect((config.presets as any).antigravity).toBeUndefined();
+    expect((config.presets as any).openai).toBeUndefined();
   });
 
-  test('generateLiteConfig uses zen-free if no antigravity or openai', () => {
+  test('generateLiteConfig uses zen-free grok-code models', () => {
     const config = generateLiteConfig({
       hasAntigravity: false,
       hasOpenAI: false,
@@ -80,10 +96,11 @@ describe('providers', () => {
 
     expect(config.preset).toBe('zen-free');
     const agents = (config.presets as any)['zen-free'];
-    expect(agents.orchestrator.model).toBe(
-      MODEL_MAPPINGS['zen-free'].orchestrator.model,
-    );
-    expect(agents.orchestrator.variant).toBeUndefined();
+    expect(agents.orchestrator.model).toBe('opencode/grok-code');
+    expect(agents.oracle.model).toBe('opencode/grok-code');
+    expect(agents.oracle.variant).toBe('high');
+    expect(agents.librarian.model).toBe('opencode/grok-code');
+    expect(agents.librarian.variant).toBe('low');
   });
 
   test('generateLiteConfig enables tmux when requested', () => {
@@ -110,4 +127,35 @@ describe('providers', () => {
     expect(agents.orchestrator.skills).toContain('*');
     expect(agents.fixer.skills).toBeDefined();
   });
+
+  test('generateLiteConfig includes mcps field', () => {
+    const config = generateLiteConfig({
+      hasAntigravity: true,
+      hasOpenAI: false,
+      hasOpencodeZen: false,
+      hasTmux: false,
+    });
+
+    const agents = (config.presets as any).antigravity;
+    expect(agents.orchestrator.mcps).toBeDefined();
+    expect(Array.isArray(agents.orchestrator.mcps)).toBe(true);
+    expect(agents.librarian.mcps).toBeDefined();
+    expect(Array.isArray(agents.librarian.mcps)).toBe(true);
+  });
+
+  test('generateLiteConfig zen-free includes correct mcps', () => {
+    const config = generateLiteConfig({
+      hasAntigravity: false,
+      hasOpenAI: false,
+      hasOpencodeZen: false,
+      hasTmux: false,
+    });
+
+    const agents = (config.presets as any)['zen-free'];
+    expect(agents.orchestrator.mcps).toContain('websearch');
+    expect(agents.librarian.mcps).toContain('websearch');
+    expect(agents.librarian.mcps).toContain('context7');
+    expect(agents.librarian.mcps).toContain('grep_app');
+    expect(agents.designer.mcps).toEqual([]);
+  });
 });

+ 42 - 41
src/cli/providers.ts

@@ -1,4 +1,7 @@
-import { DEFAULT_AGENT_SKILLS } from '../tools/skill/builtin';
+import {
+  DEFAULT_AGENT_MCPS,
+  DEFAULT_AGENT_SKILLS,
+} from '../tools/skill/builtin';
 import type { InstallConfig } from './types';
 
 /**
@@ -56,8 +59,8 @@ export const MODEL_MAPPINGS = {
     fixer: { model: 'openai/gpt-5.1-codex-mini', variant: 'low' },
   },
   'zen-free': {
-    orchestrator: { model: 'opencode/glm-4.7-free' },
-    oracle: { model: 'opencode/glm-4.7-free', variant: 'high' },
+    orchestrator: { model: 'opencode/grok-code' },
+    oracle: { model: 'opencode/grok-code', variant: 'high' },
     librarian: { model: 'opencode/grok-code', variant: 'low' },
     explorer: { model: 'opencode/grok-code', variant: 'low' },
     designer: { model: 'opencode/grok-code', variant: 'medium' },
@@ -68,24 +71,18 @@ export const MODEL_MAPPINGS = {
 export function generateLiteConfig(
   installConfig: InstallConfig,
 ): Record<string, unknown> {
-  // Determine base provider
-  const baseProvider = installConfig.hasAntigravity
-    ? 'antigravity'
-    : installConfig.hasOpenAI
-      ? 'openai'
-      : 'zen-free';
-
   const config: Record<string, unknown> = {
-    preset: baseProvider,
+    preset: 'zen-free',
     presets: {},
   };
 
-  // Generate all presets
-  for (const [providerName, models] of Object.entries(MODEL_MAPPINGS)) {
-    const agents: Record<
-      string,
-      { model: string; variant?: string; skills: string[] }
-    > = Object.fromEntries(
+  const createAgents = (
+    models: Record<string, { model: string; variant?: string }>,
+  ): Record<
+    string,
+    { model: string; variant?: string; skills: string[]; mcps: string[] }
+  > =>
+    Object.fromEntries(
       Object.entries(models).map(([k, v]) => [
         k,
         {
@@ -93,36 +90,40 @@ export function generateLiteConfig(
           variant: v.variant,
           skills:
             DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ?? [],
+          mcps: DEFAULT_AGENT_MCPS[k as keyof typeof DEFAULT_AGENT_MCPS] ?? [],
         },
       ]),
     );
-    (config.presets as Record<string, unknown>)[providerName] = agents;
-  }
-
-  // Always add antigravity-openai preset
-  const mixedAgents: Record<string, { model: string; variant?: string }> = {
-    ...MODEL_MAPPINGS.antigravity,
-  };
-  mixedAgents.oracle = { model: 'openai/gpt-5.2-codex', variant: 'high' };
-  const agents: Record<
-    string,
-    { model: string; variant?: string; skills: string[] }
-  > = Object.fromEntries(
-    Object.entries(mixedAgents).map(([k, v]) => [
-      k,
-      {
-        model: v.model,
-        variant: v.variant,
-        skills:
-          DEFAULT_AGENT_SKILLS[k as keyof typeof DEFAULT_AGENT_SKILLS] ?? [],
-      },
-    ]),
-  );
-  (config.presets as Record<string, unknown>)['antigravity-openai'] = agents;
 
-  // Set default preset based on user choice
+  // Only generate preset based on user selection
   if (installConfig.hasAntigravity && installConfig.hasOpenAI) {
+    // Mixed preset: antigravity base with OpenAI oracle
+    const mixedAgents: Record<string, { model: string; variant?: string }> = {
+      ...MODEL_MAPPINGS.antigravity,
+    };
+    mixedAgents.oracle = { model: 'openai/gpt-5.2-codex', variant: 'high' };
+    (config.presets as Record<string, unknown>)['antigravity-openai'] =
+      createAgents(mixedAgents);
+
     config.preset = 'antigravity-openai';
+  } else if (installConfig.hasAntigravity) {
+    // Antigravity only
+    (config.presets as Record<string, unknown>).antigravity = createAgents(
+      MODEL_MAPPINGS.antigravity,
+    );
+    config.preset = 'antigravity';
+  } else if (installConfig.hasOpenAI) {
+    // OpenAI only
+    (config.presets as Record<string, unknown>).openai = createAgents(
+      MODEL_MAPPINGS.openai,
+    );
+    config.preset = 'openai';
+  } else {
+    // Zen free only
+    (config.presets as Record<string, unknown>)['zen-free'] = createAgents(
+      MODEL_MAPPINGS['zen-free'],
+    );
+    config.preset = 'zen-free';
   }
 
   if (installConfig.hasTmux) {

+ 2 - 1
src/config/schema.ts

@@ -5,7 +5,8 @@ export const AgentOverrideConfigSchema = z.object({
   model: z.string().optional(),
   temperature: z.number().min(0).max(2).optional(),
   variant: z.string().optional().catch(undefined),
-  skills: z.array(z.string()).optional(), // skills this agent can use ("*" = all)
+  skills: z.array(z.string()).optional(), // skills this agent can use ("*" = all, "!item" = exclude)
+  mcps: z.array(z.string()).optional(), // MCPs this agent can use ("*" = all, "!item" = exclude)
 });
 
 // Tmux layout options

+ 39 - 5
src/index.ts

@@ -23,6 +23,7 @@ import {
 } from './tools';
 import { startTmuxCheck } from './utils';
 import { log } from './utils/logger';
+import { parseList } from './tools/skill/builtin';
 
 const OhMyOpenCodeLite: Plugin = async (ctx) => {
   const config = loadPluginConfig(ctx.directory);
@@ -96,14 +97,13 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       (opencodeConfig as { default_agent?: string }).default_agent =
         'orchestrator';
 
-      const configAgent = opencodeConfig.agent as
-        | Record<string, unknown>
-        | undefined;
-      if (!configAgent) {
+      // Merge Agent configs
+      if (!opencodeConfig.agent) {
         opencodeConfig.agent = { ...agents };
       } else {
-        Object.assign(configAgent, agents);
+        Object.assign(opencodeConfig.agent, agents);
       }
+      const configAgent = opencodeConfig.agent as Record<string, any>;
 
       // Merge MCP configs
       const configMcp = opencodeConfig.mcp as
@@ -114,6 +114,40 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       } else {
         Object.assign(configMcp, mcps);
       }
+
+      // Get all MCP names from our config
+      const allMcpNames = Object.keys(mcps);
+
+      // For each agent, create permission rules based on their mcps list
+      for (const [agentName, agentConfig] of Object.entries(agents)) {
+        const agentMcps = (agentConfig as { mcps?: string[] })?.mcps;
+        if (!agentMcps) continue;
+
+        // Get or create agent permission config
+        if (!configAgent[agentName]) {
+          configAgent[agentName] = { ...agentConfig };
+        }
+        const agentPermission = (configAgent[agentName].permission ?? {}) as Record<string, unknown>;
+
+        // Parse mcps list with wildcard and exclusion support
+        const allowedMcps = parseList(agentMcps, allMcpNames);
+
+        // Create permission rules for each MCP
+        // MCP tools are named as <server>_<tool>, so we use <server>_*
+        for (const mcpName of allMcpNames) {
+          const sanitizedMcpName = mcpName.replace(/[^a-zA-Z0-9_-]/g, '_');
+          const permissionKey = `${sanitizedMcpName}_*`;
+          const action = allowedMcps.includes(mcpName) ? 'allow' : 'deny';
+
+          // Only set if not already defined by user
+          if (!(permissionKey in agentPermission)) {
+            agentPermission[permissionKey] = action;
+          }
+        }
+
+        // Update agent config with permissions
+        configAgent[agentName].permission = agentPermission;
+      }
     },
 
     event: async (input) => {

+ 228 - 0
src/tools/grep/cli.test.ts

@@ -0,0 +1,228 @@
+/// <reference types="bun-types" />
+
+import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
+import { mkdir, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { runRg, runRgCount } from './cli';
+import { grep } from './tools';
+import { formatGrepResult } from './utils';
+
+describe('grep tool', () => {
+  const testDir = join(tmpdir(), `grep-test-${Date.now()}`);
+  const testFile1 = join(testDir, 'test1.txt');
+  const testFile2 = join(testDir, 'test2.ts');
+
+  beforeAll(async () => {
+    await mkdir(testDir, { recursive: true });
+    await writeFile(
+      testFile1,
+      'Hello world\nThis is a test file\nAnother line with match',
+    );
+    await writeFile(
+      testFile2,
+      "const x = 'Hello world';\nconsole.log('test');",
+    );
+  });
+
+  afterAll(async () => {
+    await rm(testDir, { recursive: true, force: true });
+  });
+
+  describe('formatGrepResult', () => {
+    test('formats empty results', () => {
+      const result = {
+        matches: [],
+        totalMatches: 0,
+        filesSearched: 10,
+        truncated: false,
+      };
+      expect(formatGrepResult(result)).toBe('No matches found.');
+    });
+
+    test('formats error results', () => {
+      const result = {
+        matches: [],
+        totalMatches: 0,
+        filesSearched: 0,
+        truncated: false,
+        error: 'Something went wrong',
+      };
+      expect(formatGrepResult(result)).toBe('Error: Something went wrong');
+    });
+
+    test('formats matches correctly', () => {
+      const result = {
+        matches: [
+          { file: 'file1.ts', line: 10, text: "const foo = 'bar'" },
+          { file: 'file1.ts', line: 15, text: 'console.log(foo)' },
+          { file: 'file2.ts', line: 5, text: "import { foo } from './file1'" },
+        ],
+        totalMatches: 3,
+        filesSearched: 2,
+        truncated: false,
+      };
+
+      const output = formatGrepResult(result);
+      expect(output).toContain('file1.ts:');
+      expect(output).toContain("  10: const foo = 'bar'");
+      expect(output).toContain('  15: console.log(foo)');
+      expect(output).toContain('file2.ts:');
+      expect(output).toContain("  5: import { foo } from './file1'");
+      expect(output).toContain('Found 3 matches in 2 files');
+    });
+
+    test('indicates truncation', () => {
+      const result = {
+        matches: [{ file: 'foo.txt', line: 1, text: 'bar' }],
+        totalMatches: 100,
+        filesSearched: 50,
+        truncated: true,
+      };
+      expect(formatGrepResult(result)).toContain('(output truncated)');
+    });
+  });
+
+  describe('runRg', () => {
+    test('finds matches in files', async () => {
+      const result = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+      });
+
+      expect(result.totalMatches).toBeGreaterThanOrEqual(2);
+      expect(
+        result.matches.some(
+          (m) => m.file.includes('test1.txt') && m.text.includes('Hello'),
+        ),
+      ).toBe(true);
+      expect(
+        result.matches.some(
+          (m) => m.file.includes('test2.ts') && m.text.includes('Hello'),
+        ),
+      ).toBe(true);
+    });
+
+    test('respects file inclusion patterns', async () => {
+      const result = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+        globs: ['*.txt'],
+      });
+
+      expect(result.matches.some((m) => m.file.includes('test1.txt'))).toBe(
+        true,
+      );
+      expect(result.matches.some((m) => m.file.includes('test2.ts'))).toBe(
+        false,
+      );
+    });
+
+    test('handles no matches', async () => {
+      const result = await runRg({
+        pattern: 'NonExistentString12345',
+        paths: [testDir],
+      });
+
+      expect(result.totalMatches).toBe(0);
+      expect(result.matches).toHaveLength(0);
+    });
+
+    test('respects case sensitivity', async () => {
+      // Test with exact case match
+      const resultExact = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+      });
+      expect(resultExact.totalMatches).toBeGreaterThan(0);
+
+      // Test with caseSensitive flag set to true (should not match lowercase pattern)
+      const resultSensitive = await runRg({
+        pattern: 'hello', // File has "Hello"
+        paths: [testDir],
+        caseSensitive: true,
+      });
+      expect(resultSensitive.totalMatches).toBe(0);
+    });
+
+    test('respects whole word match', async () => {
+      const resultPartial = await runRg({
+        pattern: 'Hell',
+        paths: [testDir],
+        wholeWord: false,
+      });
+      expect(resultPartial.totalMatches).toBeGreaterThan(0);
+
+      const resultWhole = await runRg({
+        pattern: 'Hell',
+        paths: [testDir],
+        wholeWord: true,
+      });
+      expect(resultWhole.totalMatches).toBe(0);
+    });
+
+    test('respects max count', async () => {
+      const result = await runRg({
+        pattern: 'Hello',
+        paths: [testDir],
+        maxCount: 1,
+      });
+      // maxCount is per file
+      expect(
+        result.matches.filter((m) => m.file.includes('test1.txt')).length,
+      ).toBeLessThanOrEqual(1);
+    });
+  });
+
+  describe('runRgCount', () => {
+    test('counts matches correctly', async () => {
+      const results = await runRgCount({
+        pattern: 'Hello',
+        paths: [testDir],
+      });
+
+      expect(results.length).toBeGreaterThan(0);
+      const file1Result = results.find((r) => r.file.includes('test1.txt'));
+      expect(file1Result).toBeDefined();
+      expect(file1Result?.count).toBe(1);
+    });
+  });
+
+  describe('grep tool execute', () => {
+    test('executes successfully', async () => {
+      // @ts-expect-error
+      const result = await grep.execute({
+        pattern: 'Hello',
+        path: testDir,
+      });
+
+      expect(typeof result).toBe('string');
+      expect(result).toContain('Found');
+      expect(result).toContain('matches');
+    });
+
+    test('handles errors gracefully', async () => {
+      // @ts-expect-error
+      const result = await grep.execute({
+        pattern: 'Hello',
+        path: '/non/existent/path/12345',
+      });
+
+      // Depending on implementation, it might return "No matches found" or an error string
+      // But it should not throw
+      expect(typeof result).toBe('string');
+    });
+
+    test('respects include pattern in execute', async () => {
+      // @ts-expect-error
+      const result = await grep.execute({
+        pattern: 'Hello',
+        path: testDir,
+        include: '*.txt',
+      });
+
+      expect(result).toContain('test1.txt');
+      expect(result).not.toContain('test2.ts');
+    });
+  });
+});

+ 171 - 0
src/tools/grep/constants.test.ts

@@ -0,0 +1,171 @@
+/// <reference types="bun-types" />
+
+import { afterEach, describe, expect, test } from 'bun:test';
+import { join } from 'node:path';
+
+// Mock process.env and process.platform for testing
+const originalPlatform = process.platform;
+const originalEnv = { ...process.env };
+
+function mockPlatform(platform: NodeJS.Platform) {
+  Object.defineProperty(process, 'platform', {
+    value: platform,
+    configurable: true,
+  });
+}
+
+function mockEnv(env: Partial<Record<string, string>>) {
+  Object.assign(process.env, env);
+}
+
+function restoreMocks() {
+  Object.defineProperty(process, 'platform', {
+    value: originalPlatform,
+    configurable: true,
+  });
+  process.env = { ...originalEnv };
+}
+
+describe('grep constants', () => {
+  afterEach(() => {
+    restoreMocks();
+  });
+
+  describe('getDataDir', () => {
+    test('returns LOCALAPPDATA on Windows when set', () => {
+      mockPlatform('win32');
+      mockEnv({ LOCALAPPDATA: 'C:\\Users\\test\\AppData\\Local' });
+
+      // Import after mocking to get the mocked behavior
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('C:\\Users\\test\\AppData\\Local');
+    });
+
+    test('returns APPDATA on Windows when LOCALAPPDATA not set', () => {
+      mockPlatform('win32');
+      mockEnv({ APPDATA: 'C:\\Users\\test\\AppData\\Roaming' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('C:\\Users\\test\\AppData\\Roaming');
+    });
+
+    test('returns USERPROFILE/AppData/Local on Windows when no env vars set', () => {
+      mockPlatform('win32');
+      mockEnv({ USERPROFILE: 'C:\\Users\\test' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('C:\\Users\\test', 'AppData', 'Local'));
+    });
+
+    test('returns XDG_DATA_HOME on Unix when set', () => {
+      mockPlatform('linux');
+      mockEnv({ XDG_DATA_HOME: '/home/test/.local/share' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('/home/test/.local/share');
+    });
+
+    test('returns HOME/.local/share on Unix when XDG_DATA_HOME not set', () => {
+      mockPlatform('linux');
+      mockEnv({ HOME: '/home/test' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('/home/test', '.local', 'share'));
+    });
+
+    test('returns ./.local/share on Unix when HOME not set', () => {
+      mockPlatform('linux');
+      // Clear HOME from environment
+      delete process.env.HOME;
+      delete process.env.XDG_DATA_HOME;
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('.', '.local', 'share'));
+    });
+
+    test('returns XDG_DATA_HOME on macOS when set', () => {
+      mockPlatform('darwin');
+      mockEnv({ XDG_DATA_HOME: '/Users/test/.local/share' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe('/Users/test/.local/share');
+    });
+
+    test('returns HOME/.local/share on macOS when XDG_DATA_HOME not set', () => {
+      mockPlatform('darwin');
+      mockEnv({ HOME: '/Users/test' });
+
+      const { getDataDir } = require('./constants');
+      const result = getDataDir();
+
+      expect(result).toBe(join('/Users/test', '.local', 'share'));
+    });
+  });
+
+  describe('constants', () => {
+    test('DEFAULT_MAX_DEPTH is 20', () => {
+      const { DEFAULT_MAX_DEPTH } = require('./constants');
+      expect(DEFAULT_MAX_DEPTH).toBe(20);
+    });
+
+    test('DEFAULT_MAX_FILESIZE is 10M', () => {
+      const { DEFAULT_MAX_FILESIZE } = require('./constants');
+      expect(DEFAULT_MAX_FILESIZE).toBe('10M');
+    });
+
+    test('DEFAULT_MAX_COUNT is 500', () => {
+      const { DEFAULT_MAX_COUNT } = require('./constants');
+      expect(DEFAULT_MAX_COUNT).toBe(500);
+    });
+
+    test('DEFAULT_MAX_COLUMNS is 1000', () => {
+      const { DEFAULT_MAX_COLUMNS } = require('./constants');
+      expect(DEFAULT_MAX_COLUMNS).toBe(1000);
+    });
+
+    test('DEFAULT_CONTEXT is 2', () => {
+      const { DEFAULT_CONTEXT } = require('./constants');
+      expect(DEFAULT_CONTEXT).toBe(2);
+    });
+
+    test('DEFAULT_TIMEOUT_MS is 300000', () => {
+      const { DEFAULT_TIMEOUT_MS } = require('./constants');
+      expect(DEFAULT_TIMEOUT_MS).toBe(300_000);
+    });
+
+    test('DEFAULT_MAX_OUTPUT_BYTES is 10MB', () => {
+      const { DEFAULT_MAX_OUTPUT_BYTES } = require('./constants');
+      expect(DEFAULT_MAX_OUTPUT_BYTES).toBe(10 * 1024 * 1024);
+    });
+
+    test('RG_SAFETY_FLAGS contains expected flags', () => {
+      const { RG_SAFETY_FLAGS } = require('./constants');
+      expect(RG_SAFETY_FLAGS).toContain('--no-follow');
+      expect(RG_SAFETY_FLAGS).toContain('--color=never');
+      expect(RG_SAFETY_FLAGS).toContain('--no-heading');
+      expect(RG_SAFETY_FLAGS).toContain('--line-number');
+      expect(RG_SAFETY_FLAGS).toContain('--with-filename');
+    });
+
+    test('GREP_SAFETY_FLAGS contains expected flags', () => {
+      const { GREP_SAFETY_FLAGS } = require('./constants');
+      expect(GREP_SAFETY_FLAGS).toContain('-n');
+      expect(GREP_SAFETY_FLAGS).toContain('-H');
+      expect(GREP_SAFETY_FLAGS).toContain('--color=never');
+    });
+  });
+});

+ 14 - 3
src/tools/grep/constants.ts

@@ -5,8 +5,6 @@ import {
   downloadAndInstallRipgrep,
   getInstalledRipgrepPath,
 } from './downloader';
-import { homedir } from 'os';
-
 
 export type GrepBackend = 'rg' | 'grep';
 
@@ -34,6 +32,19 @@ function findExecutable(name: string): string | null {
   return null;
 }
 
+export function getDataDir(): string {
+  if (process.platform === 'win32') {
+    return (
+      process.env.LOCALAPPDATA ||
+      process.env.APPDATA ||
+      join(process.env.USERPROFILE || '.', 'AppData', 'Local')
+    );
+  }
+  return (
+    process.env.XDG_DATA_HOME ||
+    join(process.env.HOME || '.', '.local', 'share')
+  );
+}
 function getOpenCodeBundledRg(): string | null {
   const execPath = process.execPath;
   const execDir = dirname(execPath);
@@ -43,7 +54,7 @@ function getOpenCodeBundledRg(): string | null {
 
   const candidates = [
     // OpenCode XDG data path (highest priority - where OpenCode installs rg)
-    join(homedir(), '.opencode', 'bin', rgName),
+    join(getDataDir(), 'opencode', 'bin', rgName),
     // Legacy paths relative to execPath
     join(execDir, rgName),
     join(execDir, 'bin', rgName),

+ 450 - 21
src/tools/skill/builtin.test.ts

@@ -1,11 +1,15 @@
 import { describe, expect, test } from 'bun:test';
 import type { PluginConfig } from '../../config/schema';
 import {
+  canAgentUseMcp,
   canAgentUseSkill,
+  DEFAULT_AGENT_MCPS,
   DEFAULT_AGENT_SKILLS,
+  getAgentMcpList,
   getBuiltinSkills,
   getSkillByName,
   getSkillsForAgent,
+  parseList,
 } from './builtin';
 
 describe('getBuiltinSkills', () => {
@@ -14,16 +18,16 @@ describe('getBuiltinSkills', () => {
     expect(skills.length).toBeGreaterThan(0);
 
     const names = skills.map((s) => s.name);
-    expect(names).toContain('yagni-enforcement');
+    expect(names).toContain('simplify');
     expect(names).toContain('playwright');
   });
 });
 
 describe('getSkillByName', () => {
   test('returns skill by exact name', () => {
-    const skill = getSkillByName('yagni-enforcement');
+    const skill = getSkillByName('simplify');
     expect(skill).toBeDefined();
-    expect(skill?.name).toBe('yagni-enforcement');
+    expect(skill?.name).toBe('simplify');
   });
 
   test('returns undefined for unknown skill', () => {
@@ -86,12 +90,12 @@ describe('getSkillsForAgent', () => {
   test('respects config override for agent skills', () => {
     const config: PluginConfig = {
       agents: {
-        oracle: { skills: ['yagni-enforcement'] },
+        oracle: { skills: ['simplify'] },
       },
     };
     const skills = getSkillsForAgent('oracle', config);
     expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('yagni-enforcement');
+    expect(skills[0].name).toBe('simplify');
   });
 
   test('config wildcard overrides default', () => {
@@ -129,12 +133,12 @@ describe('getSkillsForAgent', () => {
   test("backward compat: 'frontend-ui-ux-engineer' alias applies to designer", () => {
     const config: PluginConfig = {
       agents: {
-        'frontend-ui-ux-engineer': { skills: ['yagni-enforcement'] },
+        'frontend-ui-ux-engineer': { skills: ['simplify'] },
       },
     };
     const skills = getSkillsForAgent('designer', config);
     expect(skills.length).toBe(1);
-    expect(skills[0].name).toBe('yagni-enforcement');
+    expect(skills[0].name).toBe('simplify');
   });
 
   test('returns empty for unknown agent without config', () => {
@@ -145,31 +149,32 @@ describe('getSkillsForAgent', () => {
 
 describe('canAgentUseSkill', () => {
   test('orchestrator can use any skill (wildcard)', () => {
-    expect(canAgentUseSkill('orchestrator', 'yagni-enforcement')).toBe(true);
+    expect(canAgentUseSkill('orchestrator', 'simplify')).toBe(true);
     expect(canAgentUseSkill('orchestrator', 'playwright')).toBe(true);
-    expect(canAgentUseSkill('orchestrator', 'any-skill')).toBe(true);
+    // Note: parseList doesn't filter non-existent items when using explicit allowlist
+    // but canAgentUseSkill checks against actual skill names
   });
 
   test('designer can use playwright', () => {
     expect(canAgentUseSkill('designer', 'playwright')).toBe(true);
   });
 
-  test('designer cannot use yagni-enforcement by default', () => {
-    expect(canAgentUseSkill('designer', 'yagni-enforcement')).toBe(false);
+  test('designer cannot use simplify by default', () => {
+    expect(canAgentUseSkill('designer', 'simplify')).toBe(false);
   });
 
   test('oracle cannot use any skill by default', () => {
-    expect(canAgentUseSkill('oracle', 'yagni-enforcement')).toBe(false);
+    expect(canAgentUseSkill('oracle', 'simplify')).toBe(false);
     expect(canAgentUseSkill('oracle', 'playwright')).toBe(false);
   });
 
   test('respects config override', () => {
     const config: PluginConfig = {
       agents: {
-        oracle: { skills: ['yagni-enforcement'] },
+        oracle: { skills: ['simplify'] },
       },
     };
-    expect(canAgentUseSkill('oracle', 'yagni-enforcement', config)).toBe(true);
+    expect(canAgentUseSkill('oracle', 'simplify', config)).toBe(true);
     expect(canAgentUseSkill('oracle', 'playwright', config)).toBe(false);
   });
 
@@ -179,11 +184,9 @@ describe('canAgentUseSkill', () => {
         librarian: { skills: ['*'] },
       },
     };
-    expect(canAgentUseSkill('librarian', 'yagni-enforcement', config)).toBe(
-      true,
-    );
+    expect(canAgentUseSkill('librarian', 'simplify', config)).toBe(true);
     expect(canAgentUseSkill('librarian', 'playwright', config)).toBe(true);
-    expect(canAgentUseSkill('librarian', 'any-other-skill', config)).toBe(true);
+    // Note: parseList expands wildcard to all available skills
   });
 
   test('config empty array denies all', () => {
@@ -202,12 +205,438 @@ describe('canAgentUseSkill', () => {
       },
     };
     expect(canAgentUseSkill('explorer', 'playwright', config)).toBe(true);
-    expect(canAgentUseSkill('explorer', 'yagni-enforcement', config)).toBe(
-      false,
-    );
+    expect(canAgentUseSkill('explorer', 'simplify', config)).toBe(false);
   });
 
   test('unknown agent returns false without config', () => {
     expect(canAgentUseSkill('unknown-agent', 'playwright')).toBe(false);
   });
 });
+
+describe('parseList', () => {
+  test('returns empty array for empty input', () => {
+    expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns empty array for undefined input', () => {
+    expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns explicit items when no wildcard', () => {
+    expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('expands wildcard to all available items', () => {
+    expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
+  });
+
+  test('excludes items with ! prefix', () => {
+    expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('excludes multiple items with ! prefix', () => {
+    expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
+      'a',
+      'd',
+    ]);
+  });
+
+  test('deny wins in case of conflict', () => {
+    expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
+  });
+
+  test('!* denies all items', () => {
+    expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('!* overrides wildcard', () => {
+    expect(parseList(['*', '!*'], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('handles mixed allow and deny without wildcard', () => {
+    expect(parseList(['a', 'b', '!b'], ['a', 'b', 'c'])).toEqual(['a']);
+  });
+
+  test('excludes non-existent items gracefully', () => {
+    expect(parseList(['*', '!nonexistent'], ['a', 'b'])).toEqual(['a', 'b']);
+  });
+
+  test('returns explicit allowlist minus denials', () => {
+    expect(parseList(['a', 'd'], ['a', 'b', 'c'])).toEqual(['a', 'd']);
+  });
+});
+
+describe('DEFAULT_AGENT_MCPS', () => {
+  test('orchestrator has websearch MCP', () => {
+    expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
+  });
+
+  test('designer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
+  });
+
+  test('oracle has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
+  });
+
+  test('librarian has websearch, context7, and grep_app MCPs', () => {
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
+  });
+
+  test('explorer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
+  });
+
+  test('fixer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
+  });
+});
+
+describe('getAgentMcpList', () => {
+  test('returns default MCPs for orchestrator', () => {
+    const mcps = getAgentMcpList('orchestrator');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
+  });
+
+  test('returns default MCPs for librarian', () => {
+    const mcps = getAgentMcpList('librarian');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
+  });
+
+  test('returns empty for designer', () => {
+    const mcps = getAgentMcpList('designer');
+    expect(mcps).toEqual([]);
+  });
+
+  test('respects config override for agent MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('oracle', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('config wildcard overrides default', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    const mcps = getAgentMcpList('designer', config);
+    expect(mcps).toEqual(['*']);
+  });
+
+  test('config empty array removes default MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    const mcps = getAgentMcpList('librarian', config);
+    expect(mcps).toEqual([]);
+  });
+
+  test('backward compat: alias config applies to agent', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('explorer', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('returns empty for unknown agent without config', () => {
+    const mcps = getAgentMcpList('unknown-agent');
+    expect(mcps).toEqual([]);
+  });
+});
+
+describe('canAgentUseMcp', () => {
+  test('orchestrator can use websearch by default', () => {
+    expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
+  });
+
+  test('librarian can use websearch, context7, and grep_app by default', () => {
+    expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
+  });
+
+  test('designer cannot use any MCP by default', () => {
+    expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
+    expect(canAgentUseMcp('designer', 'context7')).toBe(false);
+  });
+
+  test('respects config override', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
+  });
+
+  test('config wildcard grants all MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
+  });
+
+  test('config wildcard grants skill MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
+  });
+
+  test('config empty array denies all MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
+  });
+
+  test('respects exclusion syntax', () => {
+    const config: PluginConfig = {
+      agents: {
+        orchestrator: { mcps: ['*', '!websearch'] },
+      },
+    };
+    // canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
+    // which is ['websearch'], so excluding websearch leaves empty
+    expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
+  });
+
+  test('backward compat: alias config affects agent permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
+  });
+
+  test('unknown agent returns false without config', () => {
+    expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
+  });
+});
+
+describe('parseList', () => {
+  test('returns empty array for empty input', () => {
+    expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns empty array for undefined input', () => {
+    expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
+  });
+
+  test('returns explicit items when no wildcard', () => {
+    expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('expands wildcard to all available items', () => {
+    expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
+  });
+
+  test('excludes items with ! prefix', () => {
+    expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
+  });
+
+  test('excludes multiple items with ! prefix', () => {
+    expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
+      'a',
+      'd',
+    ]);
+  });
+
+  test('deny wins in case of conflict', () => {
+    expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
+  });
+
+  test('!* denies all items', () => {
+    expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
+  });
+});
+
+describe('DEFAULT_AGENT_MCPS', () => {
+  test('orchestrator has websearch MCP', () => {
+    expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
+  });
+
+  test('designer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
+  });
+
+  test('oracle has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
+  });
+
+  test('librarian has websearch, context7, and grep_app MCPs', () => {
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
+    expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
+  });
+
+  test('explorer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
+  });
+
+  test('fixer has no MCPs by default', () => {
+    expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
+  });
+});
+
+describe('getAgentMcpList', () => {
+  test('returns default MCPs for orchestrator', () => {
+    const mcps = getAgentMcpList('orchestrator');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
+  });
+
+  test('returns default MCPs for librarian', () => {
+    const mcps = getAgentMcpList('librarian');
+    expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
+  });
+
+  test('returns empty for designer', () => {
+    const mcps = getAgentMcpList('designer');
+    expect(mcps).toEqual([]);
+  });
+
+  test('respects config override for agent MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('oracle', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('config wildcard overrides default', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    const mcps = getAgentMcpList('designer', config);
+    expect(mcps).toEqual(['*']);
+  });
+
+  test('config empty array removes default MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    const mcps = getAgentMcpList('librarian', config);
+    expect(mcps).toEqual([]);
+  });
+
+  test('backward compat: alias config applies to agent', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    const mcps = getAgentMcpList('explorer', config);
+    expect(mcps).toEqual(['websearch']);
+  });
+
+  test('returns empty for unknown agent without config', () => {
+    const mcps = getAgentMcpList('unknown-agent');
+    expect(mcps).toEqual([]);
+  });
+});
+
+describe('canAgentUseMcp', () => {
+  test('orchestrator can use websearch by default', () => {
+    expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
+  });
+
+  test('librarian can use websearch, context7, and grep_app by default', () => {
+    expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
+    expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
+  });
+
+  test('designer cannot use any MCP by default', () => {
+    expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
+    expect(canAgentUseMcp('designer', 'context7')).toBe(false);
+  });
+
+  test('respects config override', () => {
+    const config: PluginConfig = {
+      agents: {
+        oracle: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
+  });
+
+  test('config wildcard grants all MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
+  });
+
+  test('config wildcard grants skill MCP permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        designer: { mcps: ['*'] },
+      },
+    };
+    expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
+  });
+
+  test('config empty array denies all MCPs', () => {
+    const config: PluginConfig = {
+      agents: {
+        librarian: { mcps: [] },
+      },
+    };
+    expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
+  });
+
+  test('respects exclusion syntax', () => {
+    const config: PluginConfig = {
+      agents: {
+        orchestrator: { mcps: ['*', '!websearch'] },
+      },
+    };
+    // canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
+    // which is ['websearch'], so excluding websearch leaves empty
+    expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
+  });
+
+  test('backward compat: alias config affects agent permissions', () => {
+    const config: PluginConfig = {
+      agents: {
+        explore: { mcps: ['websearch'] },
+      },
+    };
+    expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
+    expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
+  });
+
+  test('unknown agent returns false without config', () => {
+    expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
+  });
+});

+ 114 - 15
src/tools/skill/builtin.ts

@@ -1,4 +1,5 @@
 import type { AgentName, PluginConfig } from '../../config/schema';
+import { McpNameSchema } from '../../config/schema';
 import type { SkillDefinition } from './types';
 
 /** Map old agent names to new names for backward compatibility */
@@ -7,7 +8,7 @@ const AGENT_ALIASES: Record<string, string> = {
   'frontend-ui-ux-engineer': 'designer',
 };
 
-/** Default skills per agent - "*" means all skills */
+/** Default skills per agent - "*" means all skills, "!item" excludes specific skills */
 export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
   orchestrator: ['*'],
   designer: ['playwright'],
@@ -17,7 +18,56 @@ export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
   fixer: [],
 };
 
-const YAGNI_TEMPLATE = `# YAGNI Enforcement Skill
+/** Default MCPs per agent - "*" means all MCPs, "!item" excludes specific MCPs */
+export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
+  orchestrator: ['websearch'],
+  designer: [],
+  oracle: [],
+  librarian: ['websearch', 'context7', 'grep_app'],
+  explorer: [],
+  fixer: [],
+};
+
+/**
+ * Parse a list with wildcard and exclusion syntax.
+ * Supports:
+ * - "*" to expand to all available items
+ * - "!item" to exclude specific items
+ * - Conflicts: deny wins (principle of least privilege)
+ *
+ * @param items - The list to parse (may contain "*" and "!item")
+ * @param allAvailable - All available items to expand "*" against
+ * @returns The resolved list of allowed items
+ *
+ * @example
+ * parseList(["*", "!playwright"], ["playwright", "yagni"]) // ["yagni"]
+ * parseList(["a", "c"], ["a", "b", "c"]) // ["a", "c"]
+ * parseList(["!*"], ["a", "b"]) // []
+ */
+export function parseList(items: string[], allAvailable: string[]): string[] {
+  if (!items || items.length === 0) {
+    return [];
+  }
+
+  const allow = items.filter((i) => !i.startsWith('!'));
+  const deny = items.filter((i) => i.startsWith('!')).map((i) => i.slice(1));
+
+  // Handle "!*" - deny all
+  if (deny.includes('*')) {
+    return [];
+  }
+
+  // If "*" is in allow, expand to all available minus denials
+  if (allow.includes('*')) {
+    return allAvailable.filter((item) => !deny.includes(item));
+  }
+
+  // Otherwise, return explicit allowlist minus denials
+  // Deny wins in case of conflict
+  return allow.filter((item) => !deny.includes(item));
+}
+
+const YAGNI_TEMPLATE = `# Simplify Skill
 
 You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
 
@@ -133,7 +183,7 @@ This skill provides browser automation capabilities via the Playwright MCP serve
 5. Return results with visual proof`;
 
 const yagniEnforcementSkill: SkillDefinition = {
-  name: 'yagni-enforcement',
+  name: 'simplify',
   description:
     'Code complexity analysis and YAGNI enforcement. Use after major refactors or before finalizing PRs to simplify code.',
   template: YAGNI_TEMPLATE,
@@ -165,6 +215,16 @@ export function getSkillByName(name: string): SkillDefinition | undefined {
   return builtinSkillsMap.get(name);
 }
 
+export function getAvailableMcpNames(config?: PluginConfig): string[] {
+  const builtinMcps = McpNameSchema.options;
+  const skillMcps = getBuiltinSkills().flatMap((skill) =>
+    Object.keys(skill.mcpConfig ?? {}),
+  );
+  const disabled = new Set(config?.disabled_mcps ?? []);
+  const allMcps = Array.from(new Set([...builtinMcps, ...skillMcps]));
+  return allMcps.filter((name) => !disabled.has(name));
+}
+
 /**
  * Get skills available for a specific agent
  * @param agentName - The name of the agent
@@ -175,12 +235,11 @@ export function getSkillsForAgent(
   config?: PluginConfig,
 ): SkillDefinition[] {
   const allSkills = getBuiltinSkills();
-  const agentSkills = getAgentSkillList(agentName, config);
-
-  // "*" means all skills
-  if (agentSkills.includes('*')) {
-    return allSkills;
-  }
+  const allSkillNames = allSkills.map((s) => s.name);
+  const agentSkills = parseList(
+    getAgentSkillList(agentName, config),
+    allSkillNames,
+  );
 
   return allSkills.filter((skill) => agentSkills.includes(skill.name));
 }
@@ -193,17 +252,33 @@ export function canAgentUseSkill(
   skillName: string,
   config?: PluginConfig,
 ): boolean {
-  const agentSkills = getAgentSkillList(agentName, config);
-
-  // "*" means all skills
-  if (agentSkills.includes('*')) {
-    return true;
-  }
+  const allSkills = getBuiltinSkills();
+  const allSkillNames = allSkills.map((s) => s.name);
+  const agentSkills = parseList(
+    getAgentSkillList(agentName, config),
+    allSkillNames,
+  );
 
   return agentSkills.includes(skillName);
 }
 
 /**
+ * Check if an agent can use a specific MCP
+ */
+export function canAgentUseMcp(
+  agentName: string,
+  mcpName: string,
+  config?: PluginConfig,
+): boolean {
+  const agentMcps = parseList(
+    getAgentMcpList(agentName, config),
+    getAvailableMcpNames(config),
+  );
+
+  return agentMcps.includes(mcpName);
+}
+
+/**
  * Get the skill list for an agent (from config or defaults)
  * Supports backward compatibility with old agent names via AGENT_ALIASES
  */
@@ -223,3 +298,27 @@ function getAgentSkillList(agentName: string, config?: PluginConfig): string[] {
   const defaultSkills = DEFAULT_AGENT_SKILLS[agentName as AgentName];
   return defaultSkills ?? [];
 }
+
+/**
+ * Get the MCP list for an agent (from config or defaults)
+ * Supports backward compatibility with old agent names via AGENT_ALIASES
+ */
+export function getAgentMcpList(
+  agentName: string,
+  config?: PluginConfig,
+): string[] {
+  // Check if config has override for this agent (new name first, then alias)
+  const agentConfig =
+    config?.agents?.[agentName] ??
+    config?.agents?.[
+      Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === agentName) ??
+        ''
+    ];
+  if (agentConfig?.mcps !== undefined) {
+    return agentConfig.mcps;
+  }
+
+  // Fall back to defaults
+  const defaultMcps = DEFAULT_AGENT_MCPS[agentName as AgentName];
+  return defaultMcps ?? [];
+}

+ 3 - 0
src/tools/skill/constants.ts

@@ -1,3 +1,6 @@
 export const SKILL_TOOL_DESCRIPTION = `Loads a skill and returns its instructions and available MCP tools. Use this to activate specialized capabilities like Playwright browser automation.`;
 
+export const SKILL_LIST_TOOL_DESCRIPTION =
+  'Lists skills available to the current agent.';
+
 export const SKILL_MCP_TOOL_DESCRIPTION = `Invokes a tool exposed by a skill's MCP server. Use after loading a skill to perform actions like browser automation.`;

+ 44 - 19
src/tools/skill/tools.ts

@@ -6,12 +6,14 @@ import type {
 import { type ToolDefinition, tool } from '@opencode-ai/plugin';
 import type { PluginConfig } from '../../config/schema';
 import {
+  canAgentUseMcp,
   canAgentUseSkill,
   getBuiltinSkills,
   getSkillByName,
   getSkillsForAgent,
 } from './builtin';
 import {
+  SKILL_LIST_TOOL_DESCRIPTION,
   SKILL_MCP_TOOL_DESCRIPTION,
   SKILL_TOOL_DESCRIPTION,
 } from './constants';
@@ -25,28 +27,20 @@ type ToolContext = {
   abort: AbortSignal;
 };
 
-function formatSkillsXml(skills: SkillDefinition[]): string {
-  if (skills.length === 0) return '';
+function formatSkillsList(skills: SkillDefinition[]): string {
+  if (skills.length === 0) return 'No skills available for this agent.';
 
-  const skillsXml = skills
-    .map((skill) => {
-      const lines = [
-        '  <skill>',
-        `    <name>${skill.name}</name>`,
-        `    <description>${skill.description}</description>`,
-        '  </skill>',
-      ];
-      return lines.join('\n');
-    })
+  return skills
+    .map((skill) => `- ${skill.name}: ${skill.description}`)
     .join('\n');
-
-  return `\n\n<available_skills>\n${skillsXml}\n</available_skills>`;
 }
 
 async function formatMcpCapabilities(
   skill: SkillDefinition,
   manager: SkillMcpManager,
   sessionId: string,
+  agentName: string,
+  pluginConfig?: PluginConfig,
 ): Promise<string | null> {
   if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) {
     return null;
@@ -55,6 +49,11 @@ async function formatMcpCapabilities(
   const sections: string[] = ['', '## Available MCP Servers', ''];
 
   for (const [serverName, config] of Object.entries(skill.mcpConfig)) {
+    // Check if this agent can use this MCP
+    if (!canAgentUseMcp(agentName, serverName, pluginConfig)) {
+      continue; // Skip this MCP - agent doesn't have permission
+    }
+
     const info = {
       serverName,
       skillName: skill.name,
@@ -128,11 +127,13 @@ async function formatMcpCapabilities(
 export function createSkillTools(
   manager: SkillMcpManager,
   pluginConfig?: PluginConfig,
-): { omos_skill: ToolDefinition; omos_skill_mcp: ToolDefinition } {
+): {
+  omos_skill: ToolDefinition;
+  omos_skill_list: ToolDefinition;
+  omos_skill_mcp: ToolDefinition;
+} {
   const allSkills = getBuiltinSkills();
-  const description =
-    SKILL_TOOL_DESCRIPTION +
-    (allSkills.length > 0 ? formatSkillsXml(allSkills) : '');
+  const description = SKILL_TOOL_DESCRIPTION;
 
   const skill: ToolDefinition = tool({
     description,
@@ -175,6 +176,8 @@ export function createSkillTools(
           skillDefinition,
           manager,
           sessionId,
+          agentName,
+          pluginConfig,
         );
         if (mcpInfo) {
           output.push(mcpInfo);
@@ -185,6 +188,17 @@ export function createSkillTools(
     },
   });
 
+  const skill_list: ToolDefinition = tool({
+    description: SKILL_LIST_TOOL_DESCRIPTION,
+    args: {},
+    async execute(_, toolContext) {
+      const tctx = toolContext as ToolContext | undefined;
+      const agentName = tctx?.agent ?? 'orchestrator';
+      const skills = getSkillsForAgent(agentName, pluginConfig);
+      return formatSkillsList(skills);
+    },
+  });
+
   const skill_mcp: ToolDefinition = tool({
     description: SKILL_MCP_TOOL_DESCRIPTION,
     args: {
@@ -217,6 +231,13 @@ export function createSkillTools(
         );
       }
 
+      // Check if this agent can use this MCP
+      if (!canAgentUseMcp(agentName, args.mcpName, pluginConfig)) {
+        throw new Error(
+          `Agent "${agentName}" cannot use MCP "${args.mcpName}".`,
+        );
+      }
+
       if (
         !skillDefinition.mcpConfig ||
         !skillDefinition.mcpConfig[args.mcpName]
@@ -248,5 +269,9 @@ export function createSkillTools(
     },
   });
 
-  return { omos_skill: skill, omos_skill_mcp: skill_mcp };
+  return {
+    omos_skill: skill,
+    omos_skill_list: skill_list,
+    omos_skill_mcp: skill_mcp,
+  };
 }