From 787b0687d91065cd4c4aea2f0e84ba75b457ff5c Mon Sep 17 00:00:00 2001 From: Cea <61349137+ceastld@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:00:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(integrations):=20=E6=8E=A5=E5=85=A5=20Curs?= =?UTF-8?q?or=20=E7=AD=89=E5=8A=A9=E6=89=8B=E5=B9=B6=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=AE=89=E8=A3=85=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/marketplace.json | 16 + .cursor-plugin/marketplace.json | 13 + README.md | 50 ++- .../\345\205\274\345\256\271\346\200\247.md" | 20 +- ...67\347\253\257\345\256\211\350\243\205.md" | 61 ++++ ...45\345\205\245\347\272\246\345\256\232.md" | 2 +- ...60\345\242\236\345\271\263\345\217\260.md" | 4 +- .../quicker-claude/.claude-plugin/plugin.json | 10 + plugins/quicker-claude/.mcp.json | 18 ++ plugins/quicker-claude/README.md | 9 + .../quicker-claude/scripts/quicker-mcp.ps1 | 285 ++++++++++++++++++ .../skills/write-action/SKILL.md | 28 ++ .../write-action/references/connection.md | 18 ++ .../quicker-cursor/.cursor-plugin/plugin.json | 10 + plugins/quicker-cursor/README.md | 9 + plugins/quicker-cursor/mcp.json | 18 ++ .../quicker-cursor/scripts/quicker-mcp.ps1 | 285 ++++++++++++++++++ .../skills/write-action/SKILL.md | 28 ++ .../write-action/references/connection.md | 18 ++ plugins/quicker-mcp/README.md | 9 + plugins/quicker-mcp/package.json | 6 + plugins/quicker-mcp/scripts/quicker-mcp.ps1 | 285 ++++++++++++++++++ .../quicker-mcp/skills/write-action/SKILL.md | 28 ++ .../write-action/references/connection.md | 18 ++ plugins/quicker/.codex-plugin/plugin.json | 9 +- plugins/quicker/scripts/quicker-mcp.ps1 | 16 +- plugins/quicker/skills/write-action/SKILL.md | 4 +- .../write-action/references/connection.md | 31 +- scripts/install-client.ps1 | 160 ++++++++++ scripts/sync-packages.py | 28 ++ shared/scripts/quicker-mcp.ps1 | 285 ++++++++++++++++++ shared/skills/write-action/SKILL.md | 28 ++ .../write-action/references/connection.md | 18 ++ tests/test_install_clients.py | 118 ++++++++ tests/test_quicker_mcp.py | 2 +- 35 files changed, 1903 insertions(+), 44 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .cursor-plugin/marketplace.json create mode 100644 "docs/\345\256\242\346\210\267\347\253\257\345\256\211\350\243\205.md" create mode 100644 plugins/quicker-claude/.claude-plugin/plugin.json create mode 100644 plugins/quicker-claude/.mcp.json create mode 100644 plugins/quicker-claude/README.md create mode 100644 plugins/quicker-claude/scripts/quicker-mcp.ps1 create mode 100644 plugins/quicker-claude/skills/write-action/SKILL.md create mode 100644 plugins/quicker-claude/skills/write-action/references/connection.md create mode 100644 plugins/quicker-cursor/.cursor-plugin/plugin.json create mode 100644 plugins/quicker-cursor/README.md create mode 100644 plugins/quicker-cursor/mcp.json create mode 100644 plugins/quicker-cursor/scripts/quicker-mcp.ps1 create mode 100644 plugins/quicker-cursor/skills/write-action/SKILL.md create mode 100644 plugins/quicker-cursor/skills/write-action/references/connection.md create mode 100644 plugins/quicker-mcp/README.md create mode 100644 plugins/quicker-mcp/package.json create mode 100644 plugins/quicker-mcp/scripts/quicker-mcp.ps1 create mode 100644 plugins/quicker-mcp/skills/write-action/SKILL.md create mode 100644 plugins/quicker-mcp/skills/write-action/references/connection.md create mode 100644 scripts/install-client.ps1 create mode 100644 scripts/sync-packages.py create mode 100644 shared/scripts/quicker-mcp.ps1 create mode 100644 shared/skills/write-action/SKILL.md create mode 100644 shared/skills/write-action/references/connection.md create mode 100644 tests/test_install_clients.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..63701dd --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,16 @@ +{ + "name": "quicker-agent-integrations", + "owner": { + "name": "QuickerOrg" + }, + "plugins": [ + { + "name": "quicker", + "source": "./plugins/quicker-claude", + "description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。" + } + ], + "metadata": { + "description": "通过本机 Quicker 编写和预览自动化动作的插件。" + } +} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..cd56484 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,13 @@ +{ + "name": "quicker-agent-integrations", + "owner": { + "name": "QuickerOrg" + }, + "plugins": [ + { + "name": "quicker", + "source": "./plugins/quicker-cursor", + "description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。" + } + ] +} diff --git a/README.md b/README.md index 5f850ab..2ba0c02 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,56 @@ 这个仓库集中维护各平台插件、安装与更新工具、MCP 接入约定、开发指南和契约测试。插件通过 Quicker 的公开接口工作,步骤知识由运行中的 Quicker 实时提供。 -Codex 插件 `quicker` 已提供 GitHub 市场安装入口,采用 [MIT 许可证](LICENSE)。Cursor 接入列入后续工作。 +提供 Codex、Cursor、Claude Code 插件及 VS Code / Gemini CLI 的 MCP 配置安装器,采用 [MIT 许可证](LICENSE)。 ## 支持情况 | 平台 | 状态 | 使用范围 | | --- | --- | --- | -| Codex | 已实现,v0.1.1 | Windows;读取知识、编写/保存草稿、预览 | -| Cursor | 计划接入 | 待开发和实机验证 | -| 其他 Agent | 按需求扩展 | 先确认其 MCP 和插件机制 | +| Codex | 已实现,v0.2.0 | Windows;读取知识、编写/保存草稿、预览 | +| Cursor | 本地插件;CLI 真实写动作通过 | Windows;技能、草稿创建/保存/预览 | +| Claude Code | 原生插件安装和真实 MCP 连接通过 | 尚未完成模型写动作验收 | +| VS Code / Copilot、Gemini CLI | 配置安装器 | 默认 Windows 用户配置;尚未完成各客户端写动作验收 | 需要使用设置 → Agent 中带「启用 MCP」入口、并包含默认技能包发现修复的 Quicker 新构建。Release 支持已实现,待包含这些变更的正式版发布;已发布旧版没有该入口时仍不可用。本次已验证 Debug 的草稿编写与预览,Release 配置内核测试和正式前端构建通过,正式安装包端到端仍待验收,详见[兼容性说明](docs/兼容性.md)。 +## 安装 Cursor 插件 + +在 Windows PowerShell 中执行(需要 Git): + +```powershell +$quickerInstall = Join-Path $env:TEMP ("quicker-agent-" + [guid]::NewGuid().ToString("N")) +git clone --depth 1 https://github.com/QuickerOrg/quicker-agent-integrations.git $quickerInstall +if ($LASTEXITCODE -eq 0) { + powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $quickerInstall "scripts/install-client.ps1") -Client cursor +} +``` + +安装器把自包含插件复制到 `%USERPROFILE%/.cursor/plugins/local/quicker`,运行时不依赖临时检出。执行 **Developer: Reload Window**,再新建对话,在 Cursor 插件设置确认 Quicker 的技能及 MCP 已加载。团队策略须允许本地插件导入;同名市场插件可能优先于本地版本。本项目尚未在 Cursor 官方市场上架。 + +Cursor CLI 可显式加载已安装的插件: + +```powershell +cursor-agent --plugin-dir "$env:USERPROFILE/.cursor/plugins/local/quicker" +``` + +若 PATH 中没有 `cursor-agent`,使用 Cursor CLI 安装时提供的完整命令路径。先启用 Quicker 设置 → Agent 中的 MCP 与允许写入,再让 Cursor 创建、保存并预览一个测试草稿。CLI 非交互测试还有独立的工具调用授权要求,见[各平台安装与诊断](docs/客户端安装.md)。 + +## 安装 Claude Code 插件 + +```powershell +claude plugin marketplace add QuickerOrg/quicker-agent-integrations +claude plugin install quicker@quicker-agent-integrations --scope user +``` + +执行 `/reload-plugins` 或重启 Claude Code,再用 `/mcp` 检查 Quicker。已通过 Claude Code 2.1.263 官方清单校验、本地市场安装和真实 MCP 连接检查;本机没有 Claude 登录状态,尚未完成模型写动作验收。 + +## VS Code / Copilot 与 Gemini CLI + +使用上述下载步骤,将安装器参数分别改为 `-Client vscode` 或 `-Client gemini`。安装器只合并自己的 MCP 项,保留其他服务与设置,token 始终由本地转接读取。 + +VS Code:执行 **MCP: List Servers**,启动或重启 quicker,然后在 Copilot Agent 模式试写。Gemini CLI:重启后用 `/mcp` 检查。默认配置路径、JSONC / 自定义配置的替代方式、更新与卸载见[客户端安装](docs/客户端安装.md)。本轮只验证安装与转接,未声称这些客户端已通过真实动作编写。 + ## 安装 Codex 插件 在 Windows 上执行以下两条 Codex CLI 命令,无需克隆本仓库或安装 Python: @@ -46,7 +84,7 @@ Agent 平台插件 → 查询知识 / 编辑动作草稿 / 保存 / 预览 ``` -当前 Codex 包内的 PowerShell 脚本在每次请求时读取本机 Quicker 的端口和 token,不把 token 写入插件、命令行或 Codex 配置;只请求 loopback,不使用代理或跟随重定向。它不修改 Quicker 的授权设置。 +各安装包内的 PowerShell 脚本在每次请求时读取本机 Quicker 的端口和 token,不把 token 写入插件、命令行或 Codex 配置;只请求 loopback,不使用代理或跟随重定向。它不修改 Quicker 的授权设置。 查看安装状态: @@ -101,7 +139,7 @@ tests/ test_quicker_mcp.py 独立的传输契约测试 ``` -后续平台在 `plugins/` 下增加安装单元,由对应平台的市场清单声明路径。每个安装包自包含;当第二个平台需要复用传输代码时,再抽取共享源码并在打包时放入各安装单元。 +Cursor、Claude 和 Codex 分别由自己的 marketplace 清单声明包路径。`shared/` 是传输与写动作技能的唯一源码,`python scripts/sync-packages.py` 同步到各自包含安装包,`--check` 在 CI 验证无漂移。 ## 开发与验证 diff --git "a/docs/\345\205\274\345\256\271\346\200\247.md" "b/docs/\345\205\274\345\256\271\346\200\247.md" index 89d89e8..837b3de 100644 --- "a/docs/\345\205\274\345\256\271\346\200\247.md" +++ "b/docs/\345\205\274\345\256\271\346\200\247.md" @@ -17,13 +17,15 @@ | MCP 内核测试 | 2026-09-07:Debug 41/41 通过;Release 配置 41 项覆盖通过(40 项全量通过,启停新增用例修正后单项复验通过) | | 正式前端构建 | 2026-09-07:生产前端构建与产物审计通过 | | 正式安装包端到端 | 尚未执行正式 MSI / 混淆 Release 的完整验收;不将内核测试或 Debug 草稿验收视为正式安装包已验证 | -| Cursor 与其他 Agent | 计划接入,尚未实现和验收 | +| Cursor CLI | 2026-09-07:2026.08.11-e8db854;v0.2.0 本地安装包技能加载、实时知识发现、4 步动作创建/保存/预览通过。IDE 3.7.12 的插件设置界面重载尚未单独操作验收 | +| Claude Code | 2026-09-07:2.1.263;v0.2.0 插件与市场通过官方 validator,在隔离配置中原生安装并 enabled=true;mcp list 连接真实 Quicker 成功。无 Claude 登录状态,未验证模型写动作 | +| VS Code / Gemini CLI | v0.2.0 默认用户配置安装/更新/卸载和隔离传输通过;尚未完成实际客户端写动作验收 | ## 版本记录 每个平台使用自己的插件 manifest 版本。发布记录应包含平台版本、Quicker 版本或构建、操作系统、插件版本与实测结果。尚未实测的最低版本写为未知,不猜测兼容范围。 -当前 Codex 包版本为 `0.1.1`,通过 GitHub 市场分发。旧个人市场原型使用的 `+codex.<时间戳>` 后缀只用于刷新开发缓存,不代表跨平台发布版本。 +当前各安装包版本为 `0.2.0`,通过 GitHub 市场分发。旧个人市场原型使用的 `+codex.<时间戳>` 后缀只用于刷新开发缓存,不代表跨平台发布版本。 `0.1.1` 修复 `0.1.0` 安装成功但 MCP 无法启动的问题:当前 Codex legacy MCP 加载器不会展开参数中的 `${PLUGIN_ROOT}`,现改用 `cwd: "."` 与包内相对脚本路径。旧版用户按 README 的市场升级与重新安装步骤更新。隔离 HTTP fixture 用于传输回归,真实 Quicker 验收证据见下。 @@ -51,3 +53,17 @@ Quicker MCP 服务报告版本为 `2.3.0`,但本次使用包含默认技能包 5. 单独验证只读模式和客户端拒绝访问的行为,记录现象。 只记录结果、版本及非敏感错误码。配置文件、token、真实动作库和客户端同意记录不作为测试附件上传。 + +## 0.2.0 跨平台验收 + +共享转接与技能通过 sync-packages.py 生成独立包。保留 Codex 0.1.1 已验证的相对 cwd 方式;Cursor / Claude 使用其实际支持的插件根变量。Codex 0.2.0 已通过插件校验器和原启动方式回归,未将旧客户端安装结果冒充本次更新安装。 + +Cursor CLI 从安装器生成的 `%USERPROFILE%/.cursor/plugins/local/quicker` 加载插件,读取 write-action skill 与服务端 action-source。生成“Cursor MCP 验收 · 文本摘要”,4 步完成参数文本修剪、字符数、非空行数和消息框摘要;校验保存到暂存区,quicker_preview 返回对应设计器。实际过程先出现 CLI print 模式自动拒绝;在专用测试目录授权需要的 Quicker 工具后成功,Quicker 权限和批准方式未修改。第一次源文本转义不当由 Cursor 读回发现并修正,保存结果 saved=true。动作未运行,因此这里不声称运行时统计已验收。 + +Quicker 使用 d921319283 的 Debug 构建,MCP 启用、允许写入、sandbox_confirm。新插件没有伪装已授权客户端;Client 参数分别发出 cursor-quicker-plugin / claude-quicker-plugin。所有原始日志只留本机,不提交个人配置或 token。 + +Claude Code 初次 npm 包缺少可选原生二进制;安装对应 Windows x64 2.1.263 包后官方校验、安装和 MCP 健康检查成功。首次默认 30 秒连接超时,使用该客户端 MCP_TIMEOUT=180000 复验 Connected;该变量只用于验收进程,没有修改全局权限或客户端配置。首次连接应留意 Quicker 的同意窗口。 + +第二轮 Cursor CLI 不传 --plugin-dir,仍自动发现安装器写入的本地插件。打开同一暂存动作,修改说明,保存 editVersion=4,读回说明一致并再次预览;actionId 与 slot 保持一致,没有新建或覆盖正式动作。更新后的安装包 `-Check` 返回 enabled/allowWrites/portReachable=true,无 token 内容。 + +0.2.0 最终 Windows 测试:23 项通过(16 项传输回归 + 7 项包/安装验证),覆盖两种原生插件启动参数、客户端标识、中文/空格用户目录、重复安装/更新/卸载、已有配置保留、外来同名项与本地修改拒绝、JSONC 原文不改写。 diff --git "a/docs/\345\256\242\346\210\267\347\253\257\345\256\211\350\243\205.md" "b/docs/\345\256\242\346\210\267\347\253\257\345\256\211\350\243\205.md" new file mode 100644 index 0000000..0b3edee --- /dev/null +++ "b/docs/\345\256\242\346\210\267\347\253\257\345\256\211\350\243\205.md" @@ -0,0 +1,61 @@ +# 客户端安装与诊断 + +核对日期:2026-09-07。所有连接都在 Windows 本机执行,不能让远程 Cloud Agent 访问用户电脑的 127.0.0.1。 + +## 安装机制 + +| 客户端 | 安装单元 | 加载方式 | +| --- | --- | --- | +| Cursor | plugins/quicker-cursor | `.cursor-plugin/plugin.json` + `mcp.json` + skills;本地目录或显式 `--plugin-dir` | +| Claude Code | plugins/quicker-claude | `.claude-plugin/plugin.json` + `.mcp.json` + skills;原生市场 | +| Codex | plugins/quicker | `.codex-plugin/plugin.json` + `.mcp.json` + skills;原生市场 | +| VS Code / Gemini CLI | plugins/quicker-mcp | 安装本地 stdio 转接并合并客户端 MCP JSON;编写规则来自 MCP initialize 与 skill_load | + +Cursor 和 Claude 分别展开 `${CURSOR_PLUGIN_ROOT}`、`${CLAUDE_PLUGIN_ROOT}`,Codex 使用包内相对 cwd。安装后各包独立运行,不访问 shared/ 或开发检出。 + +## 安装器的范围 + +从本仓库执行 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/install-client.ps1 -Client cursor`。支持 cursor、vscode、gemini。安装器不会打开 Quicker 权限开关。 + +- Cursor:`%USERPROFILE%/.cursor/plugins/local/quicker`。 +- VS Code 默认用户:`%USERPROFILE%/AppData/Roaming/Code/User/mcp.json` 的 `servers.quicker`。 +- Gemini CLI:`%USERPROFILE%/.gemini/settings.json` 的 `mcpServers.quicker`。 +- 通用转接安装目录:`%USERPROFILE%/.quicker/agent-integrations/`。 + +现有 quicker 项、插件目录不属于此安装器,或已安装文件被手动修改时会拒绝覆盖。安装器保留其他设置,替换前备份原配置/包,已安装包用归属标记和文件 hash 校验。备份保存在本机,不上传。JSON 不能解析时原文件不变;VS Code JSONC(注释、尾逗号)、便携版、自定义用户数据目录和 Profile 不由此安装器自动处理。 + +这些情况请使用客户端原生 MCP 配置界面:先把 plugins/quicker-mcp 复制到稳定的本机目录,再配置以下 stdio 服务,将脚本路径换成实际安装位置,Client 换成 vscode 或 gemini: + +```json +{ + "command": "powershell.exe", + "args": ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", "<安装目录>/scripts/quicker-mcp.ps1", "-Client", "vscode"] +} +``` + +VS Code 的服务项还需 `"type": "stdio"`。不填 URL、Authorization 或 token;转接自动发现本机 Quicker 设置。通过原生 UI 手工添加的条目不会被安装器接管。 + +## 更新与卸载 + +重新下载最新仓库并运行相同安装命令即可更新;安装器移走旧包,因此旧版已删除文件不会残留。已编辑安装内容时先核对差异,不能强制覆盖。 + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/install-client.ps1 -Client cursor -Uninstall +``` + +VS Code / Gemini 同样替换 Client。卸载只删除归属匹配且未编辑的当前包与对应 MCP 项,保留其他配置与旧备份。Codex 和 Claude Code 使用各自原生插件卸载命令。 + +## Cursor CLI 验收 + +已验证 Cursor IDE 3.7.12 对应机器上的 Cursor CLI 2026.08.11-e8db854,通过独立安装的本地包 `--plugin-dir` 加载技能和 `plugin-quicker-quicker` 工具;第二轮不传此参数也能自动发现并再次保存。IDE 重载后的界面发现尚未独立验收。 + +`--approve-mcps` 只同意服务器加载;非交互 `--print` 的工具调用仍有独立权限。自动化验收可在专用测试工作区 `.cursor/cli.json` 按实际需要列出 `Mcp(plugin-quicker-quicker:skill_load)`、`Mcp(plugin-quicker-quicker:quicker_create)` 等精确权限。不要给普通安装器附加全局自动批准,不要把 CLI 的自动拒绝误报为 Quicker 403。交互对话正常按客户端提示授权。 + +Quicker 的同意与运行审批仍然独立生效。使用显式 slot;保存草稿、覆盖原动作、运行不是同一结果。诊断 `-Check` 只验证配置及 TCP,不表示已经写动作成功。 + +## 官方依据 + +- [Cursor 插件清单、MCP 和本地安装](https://cursor.com/docs/reference/plugins),[本地插件目录](https://cursor.com/docs/plugins#test-plugins-locally),[CLI MCP 权限](https://cursor.com/docs/cli/reference/permissions)。 +- [Claude Code 插件与 MCP](https://code.claude.com/docs/en/plugins-reference)。 +- [VS Code MCP 配置](https://code.visualstudio.com/docs/agent-customization/mcp-servers)。 +- [Gemini CLI MCP 配置](https://geminicli.com/docs/tools/mcp-server/)。 diff --git "a/docs/\346\216\245\345\205\245\347\272\246\345\256\232.md" "b/docs/\346\216\245\345\205\245\347\272\246\345\256\232.md" index d560627..301abbf 100644 --- "a/docs/\346\216\245\345\205\245\347\272\246\345\256\232.md" +++ "b/docs/\346\216\245\345\205\245\347\272\246\345\256\232.md" @@ -2,7 +2,7 @@ 本仓库维护外部 Agent 的连接、安装与使用引导。Quicker 提供动作工具、实时知识、草稿存储和审批。各平台适配遵循本文,平台交付要求见[新增平台](新增平台.md)。 -Codex 插件 0.1.1 已通过原生安装及真实 Quicker Debug 草稿编写、保存和预览验收,Cursor 尚在规划。使用前确认 Quicker 的设置 → Agent 中有「启用 MCP」入口,并使用包含默认技能包发现修复的新构建;不能仅凭 `2.3.0` 版本号判断兼容。 +Codex 插件 0.1.1 已通过原生安装及真实 Quicker Debug 草稿编写、保存和预览验收,Cursor、Claude Code 与通用 MCP 安装器现由同一共享源码生成。使用前确认 Quicker 的设置 → Agent 中有「启用 MCP」入口,并使用包含默认技能包发现修复的新构建;不能仅凭 `2.3.0` 版本号判断兼容。 Release 支持已实现,待包含这些变更的正式版发布;已发布旧版没有 MCP 入口时仍不可用。新构建的 Debug / Release 均默认关闭 MCP,由用户在设置中启用;安装插件不会开启服务或写入权限。具体构建、测试与正式发布情况见[兼容性说明](兼容性.md)。 diff --git "a/docs/\346\226\260\345\242\236\345\271\263\345\217\260.md" "b/docs/\346\226\260\345\242\236\345\271\263\345\217\260.md" index 5f34615..6519eba 100644 --- "a/docs/\346\226\260\345\242\236\345\271\263\345\217\260.md" +++ "b/docs/\346\226\260\345\242\236\345\271\263\345\217\260.md" @@ -1,6 +1,6 @@ # 新增 Agent 平台 -本仓库在 `plugins/` 下放置完整安装单元,由各平台市场清单声明路径;当前 Codex 使用 `plugins/quicker/`。共同语义遵循[接入约定](接入约定.md)。Cursor 仅为规划;增加目录或配置示例不等于平台已经支持。 +本仓库在 `plugins/` 下放置完整安装单元,由各平台市场清单声明路径;当前 Codex 使用 `plugins/quicker/`。共同语义遵循[接入约定](接入约定.md)。现有 Cursor / Claude 包可作平台清单示例;增加目录或配置示例不等于真实客户端已经验收。 ## 先确认接入方式 @@ -27,7 +27,7 @@ ## 共享的时机 -当前转接脚本保留在 Codex 包内。出现第二个实际消费者后,再按已有差异提取公共实现:优先共享 MCP 转接和契约测试,清单、平台身份、安装器及客户端重载提示仍留在平台层。 +当前 shared/ 保存转接与技能唯一源码,sync-packages.py 复制到 Codex / Cursor / Claude / 通用 MCP 安装单元。平台身份由受限 Client 参数指定;清单与重载方式各自维护。 若提取到 `shared/`,指定唯一源码,通过一个确定的打包步骤复制到各安装单元;验证复制结果与源码一致,并测试最终包。不要手工维护多份转接实现,也不要为尚未开发的平台预建抽象框架。 diff --git a/plugins/quicker-claude/.claude-plugin/plugin.json b/plugins/quicker-claude/.claude-plugin/plugin.json new file mode 100644 index 0000000..88f1309 --- /dev/null +++ b/plugins/quicker-claude/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "quicker", + "version": "0.2.0", + "description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。", + "author": { + "name": "QuickerOrg" + }, + "license": "MIT", + "repository": "https://github.com/QuickerOrg/quicker-agent-integrations" +} diff --git a/plugins/quicker-claude/.mcp.json b/plugins/quicker-claude/.mcp.json new file mode 100644 index 0000000..45ee420 --- /dev/null +++ b/plugins/quicker-claude/.mcp.json @@ -0,0 +1,18 @@ +{ + "mcpServers": { + "quicker": { + "command": "powershell.exe", + "args": [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + "${CLAUDE_PLUGIN_ROOT}/scripts/quicker-mcp.ps1", + "-Client", + "claude" + ] + } + } +} diff --git a/plugins/quicker-claude/README.md b/plugins/quicker-claude/README.md new file mode 100644 index 0000000..b07d322 --- /dev/null +++ b/plugins/quicker-claude/README.md @@ -0,0 +1,9 @@ +# Quicker claude 接入包 + +Windows PowerShell 5.1 + 支持 MCP 的本机 Quicker。此目录是完整运行单元,不依赖开发检出。 + +安装、更新、卸载和实测兼容状态见 [公开仓库](https://github.com/QuickerOrg/quicker-agent-integrations#readme) 与 [客户端安装](https://github.com/QuickerOrg/quicker-agent-integrations/blob/main/docs/客户端安装.md)。 + +启用 Quicker 设置 → Agent 中的 MCP 与允许写入,然后重新加载客户端并完成 Quicker 客户端授权。插件读取实时动作知识,默认把结果保存到暂存区;安装本身不授予执行或覆盖权限。运行时不需要 Git、Python 或 Node。 + +传输脚本与技能由 shared/ 生成;维护时运行 scripts/sync-packages.py,不手工修改副本。 diff --git a/plugins/quicker-claude/scripts/quicker-mcp.ps1 b/plugins/quicker-claude/scripts/quicker-mcp.ps1 new file mode 100644 index 0000000..3811fe8 --- /dev/null +++ b/plugins/quicker-claude/scripts/quicker-mcp.ps1 @@ -0,0 +1,285 @@ +[CmdletBinding()] +param( + [string]$SettingsPath = '', + [ValidateRange(1, 600)] + [int]$RequestTimeoutSeconds = 180, + [ValidateSet('codex', 'cursor', 'claude', 'vscode', 'gemini')] + [string]$Client = 'codex', + [switch]$Check +) + +# A transport adapter only: Quicker owns tools, authoring state and approval policy. +# Keep JSON payloads as text so Windows PowerShell never rewrites schemas or numbers. +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$utf8 = New-Object System.Text.UTF8Encoding($false) +[Console]::InputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 +$OutputEncoding = $utf8 +Add-Type -AssemblyName System.Web.Extensions +$jsonReader = New-Object System.Web.Script.Serialization.JavaScriptSerializer +$jsonReader.MaxJsonLength = [int]::MaxValue +$jsonReader.RecursionLimit = 256 +if ([string]::IsNullOrWhiteSpace($SettingsPath)) { + $SettingsPath = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.quicker\mcp\server.json' +} + +function Write-ProtocolLine([string]$Text) { + [Console]::Out.WriteLine($Text) + [Console]::Out.Flush() +} + +function Compress-JsonText([string]$Text) { + $builder = New-Object System.Text.StringBuilder + $inString = $false + $escaped = $false + foreach ($character in $Text.ToCharArray()) { + if ($inString) { + [void]$builder.Append($character) + if ($escaped) { $escaped = $false } + elseif ($character -eq '\') { $escaped = $true } + elseif ($character -eq '"') { $inString = $false } + } + elseif ($character -eq '"') { + $inString = $true + [void]$builder.Append($character) + } + elseif (-not [char]::IsWhiteSpace($character)) { [void]$builder.Append($character) } + } + return $builder.ToString() +} + +function Get-RpcIdText([string]$Text) { + # Read only the top-level id token, preserving a numeric id's exact spelling. + $depth = 0 + for ($index = 0; $index -lt $Text.Length; $index++) { + $character = $Text[$index] + if ($character -eq '{' -or $character -eq '[') { $depth++; continue } + if ($character -eq '}' -or $character -eq ']') { $depth--; continue } + if ($character -ne '"') { continue } + $start = $index + $index++ + for (; $index -lt $Text.Length; $index++) { + if ($Text[$index] -eq '\') { $index++; continue } + if ($Text[$index] -eq '"') { break } + } + if ($depth -ne 1) { continue } + $after = $index + 1 + while ($after -lt $Text.Length -and [char]::IsWhiteSpace($Text[$after])) { $after++ } + if ($after -ge $Text.Length -or $Text[$after] -ne ':') { continue } + $keyToken = $Text.Substring($start, $index - $start + 1) + $key = $jsonReader.DeserializeObject('{"key":' + $keyToken + '}') + if ($key.key -cne 'id') { continue } + $valueStart = $after + 1 + while ($valueStart -lt $Text.Length -and [char]::IsWhiteSpace($Text[$valueStart])) { $valueStart++ } + $valueEnd = $valueStart + if ($Text[$valueStart] -eq '"') { + $valueEnd++ + for (; $valueEnd -lt $Text.Length; $valueEnd++) { + if ($Text[$valueEnd] -eq '\') { $valueEnd++; continue } + if ($Text[$valueEnd] -eq '"') { $valueEnd++; break } + } + } + else { + while ($valueEnd -lt $Text.Length -and $Text[$valueEnd] -ne ',' -and $Text[$valueEnd] -ne '}') { $valueEnd++ } + } + return $Text.Substring($valueStart, $valueEnd - $valueStart).Trim() + } + return $null +} + +function Write-RpcFailure { + param([string]$IdText, [int]$Code, [string]$FailureCode, [string]$Message, + [bool]$StateUnknown = $false, [int]$HttpStatus = 0) + if ([string]::IsNullOrEmpty($IdText)) { + # Notifications must not receive JSON-RPC replies. Diagnostics never contain input data. + [Console]::Error.WriteLine('Quicker MCP: ' + $FailureCode + '. ' + $Message) + return + } + $data = [ordered]@{ code = $FailureCode; stateUnknown = $StateUnknown } + if ($HttpStatus -ne 0) { $data.httpStatus = $HttpStatus } + $errorBody = [ordered]@{ code = $Code; message = $Message; data = $data } | ConvertTo-Json -Depth 8 -Compress + Write-ProtocolLine ('{"jsonrpc":"2.0","id":' + $IdText + ',"error":' + $errorBody + '}') +} + +function Read-McpSettings { + if (-not [System.IO.File]::Exists($SettingsPath)) { + return @{ Code = 'settings_missing'; Message = 'Enable MCP Server in a Debug build of Quicker first.' } + } + try { + $settings = $jsonReader.DeserializeObject([System.IO.File]::ReadAllText($SettingsPath, $utf8)) + if ($null -eq $settings -or $settings -is [array]) { throw 'invalid' } + $port = 0 + if (-not [int]::TryParse([string]$settings.Port, [ref]$port) -or $port -lt 1024 -or $port -gt 65535) { + throw 'invalid' + } + $tokenPresent = $settings.Token -is [string] -and -not [string]::IsNullOrWhiteSpace($settings.Token) + if ($tokenPresent -and $settings.Token -match '\s') { throw 'invalid' } + $enabled = $settings.Enabled -is [bool] -and $settings.Enabled + $allowWrites = $settings.AllowWrites -is [bool] -and $settings.AllowWrites + return @{ Enabled = $enabled; AllowWrites = $allowWrites; Port = $port; + TokenPresent = $tokenPresent; Token = $settings.Token } + } + catch { + return @{ Code = 'settings_invalid'; Message = 'Quicker MCP settings could not be read. Check them in Quicker.' } + } +} + +if ($Check) { + $settings = Read-McpSettings + if ($settings.Code) { + Write-ProtocolLine (([ordered]@{ ok = $false; code = $settings.Code; message = $settings.Message }) | ConvertTo-Json -Compress) + exit 1 + } + $reachable = $false + $tcpClient = New-Object System.Net.Sockets.TcpClient + try { + $pending = $tcpClient.BeginConnect('127.0.0.1', $settings.Port, $null, $null) + if ($pending.AsyncWaitHandle.WaitOne(2000)) { + $tcpClient.EndConnect($pending) + $reachable = $tcpClient.Connected + } + $pending.AsyncWaitHandle.Close() + } + catch { $reachable = $false } + finally { $tcpClient.Close() } + $ready = $settings.Enabled -and $settings.TokenPresent -and $reachable + Write-ProtocolLine (([ordered]@{ ok = [bool]$ready; enabled = [bool]$settings.Enabled; + allowWrites = [bool]$settings.AllowWrites; port = $settings.Port; + tokenPresent = [bool]$settings.TokenPresent; portReachable = $reachable }) | ConvertTo-Json -Compress) + if ($ready) { exit 0 } + exit 1 +} + +$protocolVersion = $null +while ($null -ne ($line = [Console]::In.ReadLine())) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $idText = $null + try { + $rpc = $jsonReader.DeserializeObject($line) + $idText = Get-RpcIdText $line + if ($null -eq $rpc -or $rpc -is [array] -or $rpc.jsonrpc -cne '2.0' -or + $rpc.method -isnot [string] -or [string]::IsNullOrWhiteSpace($rpc.method)) { + throw 'invalid request' + } + if ($null -ne $rpc.id -and ($rpc.id -is [bool] -or ($rpc.id -isnot [string] -and $rpc.id -isnot [ValueType]))) { throw 'invalid id' } + } + catch { + Write-RpcFailure -IdText 'null' -Code -32600 -FailureCode 'invalid_request' -Message 'Expected one JSON-RPC 2.0 request or notification per line.' + continue + } + + $settings = Read-McpSettings + if ($settings.Code) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode $settings.Code -Message $settings.Message + continue + } + if (-not $settings.Enabled) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'server_disabled' -Message 'Enable MCP Server in Quicker settings.' + continue + } + if (-not $settings.TokenPresent) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'token_missing' -Message 'Quicker MCP has no token. Check MCP Server settings in Quicker.' + continue + } + + $request = $null + $response = $null + $mayHaveSent = $false + $forwardedResponse = $false + try { + # Only the persisted port is configurable. Never send the token to another host, + # an HTTP proxy, or a redirect target. + $request = [System.Net.HttpWebRequest]::Create('http://127.0.0.1:' + $settings.Port + '/mcp') + $request.Method = 'POST' + $request.Proxy = $null + $request.AllowAutoRedirect = $false + # Avoid automatic replay when a reused keep-alive socket is closed by the server. + $request.KeepAlive = $false + $request.Timeout = $RequestTimeoutSeconds * 1000 + $request.ReadWriteTimeout = $RequestTimeoutSeconds * 1000 + $request.ContentType = 'application/json; charset=utf-8' + $request.Accept = 'application/json, text/event-stream' + $request.Headers['Authorization'] = 'Bearer ' + $settings.Token + $request.Headers['Mcp-Client-Info'] = ($Client + '-quicker-plugin') + if ($protocolVersion) { $request.Headers['MCP-Protocol-Version'] = $protocolVersion } + $request.ServicePoint.Expect100Continue = $false + $bytes = $utf8.GetBytes($line) + $request.ContentLength = $bytes.Length + $mayHaveSent = $true + $stream = $request.GetRequestStream() + try { $stream.Write($bytes, 0, $bytes.Length) } + finally { $stream.Dispose() } + try { $response = $request.GetResponse() } + catch [System.Net.WebException] { + if ($null -eq $_.Exception.Response) { throw } + $response = $_.Exception.Response + } + $status = [int]$response.StatusCode + if ($status -lt 200 -or $status -ge 300) { + $failureCode = 'http_error' + $message = 'Quicker MCP rejected the HTTP request. Check Quicker before retrying.' + $stateUnknown = $status -ge 500 + if ($status -eq 401) { $failureCode = 'unauthorized'; $message = 'Quicker rejected the MCP token. Check MCP settings in Quicker.' } + elseif ($status -eq 403) { $failureCode = 'client_not_approved'; $message = 'Approve the connecting MCP client in Quicker, then reconnect.' } + elseif ($status -ge 300 -and $status -lt 400) { $failureCode = 'redirect_refused'; $message = 'Quicker MCP returned a redirect. Redirects are refused; check the local endpoint.' } + Write-RpcFailure -IdText $idText -Code -32003 -FailureCode $failureCode -Message $message -StateUnknown $stateUnknown -HttpStatus $status + continue + } + if ($status -eq 202) { + if ($idText) { throw 'missing response' } + continue + } + $reader = New-Object System.IO.StreamReader($response.GetResponseStream(), $utf8) + try { $body = $reader.ReadToEnd() } + finally { $reader.Dispose() } + if ([string]::IsNullOrWhiteSpace($body)) { + if ($idText) { throw 'missing response' } + continue + } + $payloads = New-Object 'System.Collections.Generic.List[string]' + if ($response.ContentType -match '^text/event-stream(?:;|$)') { + $eventData = New-Object 'System.Collections.Generic.List[string]' + foreach ($eventLine in ($body -split '\r\n|\n|\r')) { + if ($eventLine.Length -eq 0) { + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")); $eventData.Clear() } + } + elseif ($eventLine.StartsWith('data:')) { + $value = $eventLine.Substring(5) + if ($value.StartsWith(' ')) { $value = $value.Substring(1) } + $eventData.Add($value) + } + } + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")) } + } + elseif ($response.ContentType -match '^application/json(?:;|$)') { $payloads.Add($body) } + else { throw 'unsupported response' } + + foreach ($payload in $payloads) { + $parsed = $jsonReader.DeserializeObject($payload) + if ($null -eq $parsed -or $parsed -is [array] -or $parsed.jsonrpc -cne '2.0') { throw 'invalid response' } + $responseId = Get-RpcIdText $payload + if ($null -ne $responseId) { + if (-not $idText -or $forwardedResponse -or $parsed.id -cne $rpc.id) { throw 'unexpected response id' } + if ($rpc.method -ceq 'initialize' -and $parsed.result.protocolVersion -is [string]) { + $version = $parsed.result.protocolVersion + if ($version -match '^\d{4}-\d{2}-\d{2}$') { $protocolVersion = $version } + } + $forwardedResponse = $true + } + elseif ($parsed.method -isnot [string]) { throw 'invalid notification' } + Write-ProtocolLine (Compress-JsonText $payload) + } + if ($idText -and -not $forwardedResponse) { throw 'missing response' } + } + catch { + if (-not $forwardedResponse) { + Write-RpcFailure -IdText $idText -Code -32002 -FailureCode 'transport_failed' -StateUnknown $mayHaveSent -Message 'The Quicker MCP request did not produce a complete response. Its outcome may be unknown; inspect Quicker and reopen or reread the action before retrying a write.' + } + else { [Console]::Error.WriteLine('Quicker MCP: invalid data after the completed response.') } + } + finally { + if ($null -ne $response) { $response.Dispose() } + if ($null -ne $request) { $request.Abort() } + } +} diff --git a/plugins/quicker-claude/skills/write-action/SKILL.md b/plugins/quicker-claude/skills/write-action/SKILL.md new file mode 100644 index 0000000..6900424 --- /dev/null +++ b/plugins/quicker-claude/skills/write-action/SKILL.md @@ -0,0 +1,28 @@ +--- +name: write-action +description: Create, edit, save and preview Windows automation actions in a running Quicker through its MCP tools. Use when the user asks to write or modify a Quicker action, or consult Quicker step knowledge. Does not apply to developing the Quicker application's source code. +--- + +# Write a Quicker action + +Use the Quicker plugin's MCP tools, whose names may have a client namespace prefix. These edit Quicker's virtual workspace, not the agent repository. In particular, use Quicker's read_file, write_file, edit_file and grep for slots and @knowledge; ordinary local filesystem tools cannot resolve them. + +The live server's initialize instructions and returned capability rules describe the current authoring protocol. Read all content blocks, including attachedRules. Tool availability and current knowledge are authoritative; this plugin does not carry a duplicate module catalog. + +## Authoring workflow + +1. Load the runtime grammar using skill_load with `{ "id": "action-source" }` before editing. For an unfamiliar step, follow its steps skill: grep one keyword under @knowledge/catalog, then read the matching module YAML. Use quicker_eval with steps.resolve only for the needed branch or referenced docs. Do not invent step names or input/output keys. Host JS uses JavaScript; action `$=` expressions use Quicker's C# expression syntax. +2. For a new action use quicker_create with a concise title. To edit an existing one, use quicker_search once, identify the intended result, then quicker_open with its actionId. Opening a formal action creates an editing draft; it does not overwrite the original. +3. Keep the returned slot and actionId. Always address files explicitly as `/program.source.yaml`, `/info.yaml` or `/files/...`, and save with quicker_save `{ "path": "" }`. Several tasks can share an agent backend process and Quicker's current location; do not rely on the implicit working directory or concurrently edit the same slot. +4. Write the program in program.source.yaml; use info.yaml for title, description and appearance. data.json and info.json are Host internals. quicker_save validates and stores the draft in Quicker's 暂存区. Inspect its result and correct reported errors before reporting completion. +5. Use quicker_preview `{ "target": "" }` when showing the result helps the user. It opens Quicker's designer. Only open a browser URL actually returned by the tool; do not construct one from a designer route. + +A request to write an action normally ends with a saved draft. Promote a new draft only when the user has specified an unambiguous existing destination scene; otherwise explain the 暂存区 → 保留到… flow. Overwrite a formal action or run an action only within the user's requested scope, using quicker_overwrite or quicker_run and Quicker's approval flow. State whether the action was only saved, also previewed, or actually run. + +## Failure handling + +Check JSON-RPC errors, MCP isError, `{ok:false,code,message}`, and quicker_eval's `{error:...}` envelope. Do not infer success from receiving text. + +When a mutation times out, the connection drops, or stateUnknown is returned, inspect the relevant slot/action before retrying. If create reports programCreated with an actionId, reopen that action rather than creating another. For catalog_stale or catalog_freshness_unknown, inspect the current action and preserve pending changes; do not blindly overwrite or re-open over dirty work. + +If tools are unavailable, consult [connection.md](references/connection.md). A missing write tool usually means Quicker's 允许 MCP 写入 switch is off. The plugin must not edit server.json or clients.json to enable access, grant consent, or change approval settings. diff --git a/plugins/quicker-claude/skills/write-action/references/connection.md b/plugins/quicker-claude/skills/write-action/references/connection.md new file mode 100644 index 0000000..04ed0f4 --- /dev/null +++ b/plugins/quicker-claude/skills/write-action/references/connection.md @@ -0,0 +1,18 @@ +# Connection troubleshooting + +Requires Windows PowerShell 5.1 and a Quicker build with Settings → Agent → Enable MCP. Both supported Debug and Release builds work; older releases without that setting do not. + +Run powershell.exe -NoProfile -ExecutionPolicy Bypass -File "/scripts/quicker-mcp.ps1" -Check for a redacted configuration/TCP check. This does not prove client consent or write access. + +The relay reads the current port and token from the local Quicker configuration for each request. Keep the token out of commands, client configuration, logs and conversation. It uses only loopback and does not follow proxies or redirects. + +- Missing/disabled MCP: open Quicker Settings → Agent and enable MCP. +- Connection refused: start the supported Quicker build and check the configured port. +- HTTP 401: check the running instance; token rotation is picked up on the next read. +- HTTP 403 or a first connection waiting: complete Quicker's consent UI for the actual connecting client. The relay never grants itself access. +- Write tools absent: enable 允许 MCP 写入 in Quicker, keep the existing approval mode, then reconnect or start a new agent task. +- A write timeout can leave an unknown outcome: inspect the slot before retrying. + +After installation/update, reload the client and create a new task. Cursor IDE: Developer: Reload Window; Cursor CLI: restart agent. Claude Code: /reload-plugins or restart. VS Code: MCP: List Servers → Quicker → Restart. Gemini CLI: restart. + +Plugins ship their own relay. Cursor and Claude expand their respective plugin-root variables; Codex uses package-relative cwd. Generic MCP configuration points at the installed copy, never the development checkout. Do not edit server.json or clients.json to grant access. diff --git a/plugins/quicker-cursor/.cursor-plugin/plugin.json b/plugins/quicker-cursor/.cursor-plugin/plugin.json new file mode 100644 index 0000000..88f1309 --- /dev/null +++ b/plugins/quicker-cursor/.cursor-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "quicker", + "version": "0.2.0", + "description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。", + "author": { + "name": "QuickerOrg" + }, + "license": "MIT", + "repository": "https://github.com/QuickerOrg/quicker-agent-integrations" +} diff --git a/plugins/quicker-cursor/README.md b/plugins/quicker-cursor/README.md new file mode 100644 index 0000000..82eb359 --- /dev/null +++ b/plugins/quicker-cursor/README.md @@ -0,0 +1,9 @@ +# Quicker cursor 接入包 + +Windows PowerShell 5.1 + 支持 MCP 的本机 Quicker。此目录是完整运行单元,不依赖开发检出。 + +安装、更新、卸载和实测兼容状态见 [公开仓库](https://github.com/QuickerOrg/quicker-agent-integrations#readme) 与 [客户端安装](https://github.com/QuickerOrg/quicker-agent-integrations/blob/main/docs/客户端安装.md)。 + +启用 Quicker 设置 → Agent 中的 MCP 与允许写入,然后重新加载客户端并完成 Quicker 客户端授权。插件读取实时动作知识,默认把结果保存到暂存区;安装本身不授予执行或覆盖权限。运行时不需要 Git、Python 或 Node。 + +传输脚本与技能由 shared/ 生成;维护时运行 scripts/sync-packages.py,不手工修改副本。 diff --git a/plugins/quicker-cursor/mcp.json b/plugins/quicker-cursor/mcp.json new file mode 100644 index 0000000..2675e69 --- /dev/null +++ b/plugins/quicker-cursor/mcp.json @@ -0,0 +1,18 @@ +{ + "mcpServers": { + "quicker": { + "command": "powershell.exe", + "args": [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + "${CURSOR_PLUGIN_ROOT}/scripts/quicker-mcp.ps1", + "-Client", + "cursor" + ] + } + } +} diff --git a/plugins/quicker-cursor/scripts/quicker-mcp.ps1 b/plugins/quicker-cursor/scripts/quicker-mcp.ps1 new file mode 100644 index 0000000..3811fe8 --- /dev/null +++ b/plugins/quicker-cursor/scripts/quicker-mcp.ps1 @@ -0,0 +1,285 @@ +[CmdletBinding()] +param( + [string]$SettingsPath = '', + [ValidateRange(1, 600)] + [int]$RequestTimeoutSeconds = 180, + [ValidateSet('codex', 'cursor', 'claude', 'vscode', 'gemini')] + [string]$Client = 'codex', + [switch]$Check +) + +# A transport adapter only: Quicker owns tools, authoring state and approval policy. +# Keep JSON payloads as text so Windows PowerShell never rewrites schemas or numbers. +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$utf8 = New-Object System.Text.UTF8Encoding($false) +[Console]::InputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 +$OutputEncoding = $utf8 +Add-Type -AssemblyName System.Web.Extensions +$jsonReader = New-Object System.Web.Script.Serialization.JavaScriptSerializer +$jsonReader.MaxJsonLength = [int]::MaxValue +$jsonReader.RecursionLimit = 256 +if ([string]::IsNullOrWhiteSpace($SettingsPath)) { + $SettingsPath = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.quicker\mcp\server.json' +} + +function Write-ProtocolLine([string]$Text) { + [Console]::Out.WriteLine($Text) + [Console]::Out.Flush() +} + +function Compress-JsonText([string]$Text) { + $builder = New-Object System.Text.StringBuilder + $inString = $false + $escaped = $false + foreach ($character in $Text.ToCharArray()) { + if ($inString) { + [void]$builder.Append($character) + if ($escaped) { $escaped = $false } + elseif ($character -eq '\') { $escaped = $true } + elseif ($character -eq '"') { $inString = $false } + } + elseif ($character -eq '"') { + $inString = $true + [void]$builder.Append($character) + } + elseif (-not [char]::IsWhiteSpace($character)) { [void]$builder.Append($character) } + } + return $builder.ToString() +} + +function Get-RpcIdText([string]$Text) { + # Read only the top-level id token, preserving a numeric id's exact spelling. + $depth = 0 + for ($index = 0; $index -lt $Text.Length; $index++) { + $character = $Text[$index] + if ($character -eq '{' -or $character -eq '[') { $depth++; continue } + if ($character -eq '}' -or $character -eq ']') { $depth--; continue } + if ($character -ne '"') { continue } + $start = $index + $index++ + for (; $index -lt $Text.Length; $index++) { + if ($Text[$index] -eq '\') { $index++; continue } + if ($Text[$index] -eq '"') { break } + } + if ($depth -ne 1) { continue } + $after = $index + 1 + while ($after -lt $Text.Length -and [char]::IsWhiteSpace($Text[$after])) { $after++ } + if ($after -ge $Text.Length -or $Text[$after] -ne ':') { continue } + $keyToken = $Text.Substring($start, $index - $start + 1) + $key = $jsonReader.DeserializeObject('{"key":' + $keyToken + '}') + if ($key.key -cne 'id') { continue } + $valueStart = $after + 1 + while ($valueStart -lt $Text.Length -and [char]::IsWhiteSpace($Text[$valueStart])) { $valueStart++ } + $valueEnd = $valueStart + if ($Text[$valueStart] -eq '"') { + $valueEnd++ + for (; $valueEnd -lt $Text.Length; $valueEnd++) { + if ($Text[$valueEnd] -eq '\') { $valueEnd++; continue } + if ($Text[$valueEnd] -eq '"') { $valueEnd++; break } + } + } + else { + while ($valueEnd -lt $Text.Length -and $Text[$valueEnd] -ne ',' -and $Text[$valueEnd] -ne '}') { $valueEnd++ } + } + return $Text.Substring($valueStart, $valueEnd - $valueStart).Trim() + } + return $null +} + +function Write-RpcFailure { + param([string]$IdText, [int]$Code, [string]$FailureCode, [string]$Message, + [bool]$StateUnknown = $false, [int]$HttpStatus = 0) + if ([string]::IsNullOrEmpty($IdText)) { + # Notifications must not receive JSON-RPC replies. Diagnostics never contain input data. + [Console]::Error.WriteLine('Quicker MCP: ' + $FailureCode + '. ' + $Message) + return + } + $data = [ordered]@{ code = $FailureCode; stateUnknown = $StateUnknown } + if ($HttpStatus -ne 0) { $data.httpStatus = $HttpStatus } + $errorBody = [ordered]@{ code = $Code; message = $Message; data = $data } | ConvertTo-Json -Depth 8 -Compress + Write-ProtocolLine ('{"jsonrpc":"2.0","id":' + $IdText + ',"error":' + $errorBody + '}') +} + +function Read-McpSettings { + if (-not [System.IO.File]::Exists($SettingsPath)) { + return @{ Code = 'settings_missing'; Message = 'Enable MCP Server in a Debug build of Quicker first.' } + } + try { + $settings = $jsonReader.DeserializeObject([System.IO.File]::ReadAllText($SettingsPath, $utf8)) + if ($null -eq $settings -or $settings -is [array]) { throw 'invalid' } + $port = 0 + if (-not [int]::TryParse([string]$settings.Port, [ref]$port) -or $port -lt 1024 -or $port -gt 65535) { + throw 'invalid' + } + $tokenPresent = $settings.Token -is [string] -and -not [string]::IsNullOrWhiteSpace($settings.Token) + if ($tokenPresent -and $settings.Token -match '\s') { throw 'invalid' } + $enabled = $settings.Enabled -is [bool] -and $settings.Enabled + $allowWrites = $settings.AllowWrites -is [bool] -and $settings.AllowWrites + return @{ Enabled = $enabled; AllowWrites = $allowWrites; Port = $port; + TokenPresent = $tokenPresent; Token = $settings.Token } + } + catch { + return @{ Code = 'settings_invalid'; Message = 'Quicker MCP settings could not be read. Check them in Quicker.' } + } +} + +if ($Check) { + $settings = Read-McpSettings + if ($settings.Code) { + Write-ProtocolLine (([ordered]@{ ok = $false; code = $settings.Code; message = $settings.Message }) | ConvertTo-Json -Compress) + exit 1 + } + $reachable = $false + $tcpClient = New-Object System.Net.Sockets.TcpClient + try { + $pending = $tcpClient.BeginConnect('127.0.0.1', $settings.Port, $null, $null) + if ($pending.AsyncWaitHandle.WaitOne(2000)) { + $tcpClient.EndConnect($pending) + $reachable = $tcpClient.Connected + } + $pending.AsyncWaitHandle.Close() + } + catch { $reachable = $false } + finally { $tcpClient.Close() } + $ready = $settings.Enabled -and $settings.TokenPresent -and $reachable + Write-ProtocolLine (([ordered]@{ ok = [bool]$ready; enabled = [bool]$settings.Enabled; + allowWrites = [bool]$settings.AllowWrites; port = $settings.Port; + tokenPresent = [bool]$settings.TokenPresent; portReachable = $reachable }) | ConvertTo-Json -Compress) + if ($ready) { exit 0 } + exit 1 +} + +$protocolVersion = $null +while ($null -ne ($line = [Console]::In.ReadLine())) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $idText = $null + try { + $rpc = $jsonReader.DeserializeObject($line) + $idText = Get-RpcIdText $line + if ($null -eq $rpc -or $rpc -is [array] -or $rpc.jsonrpc -cne '2.0' -or + $rpc.method -isnot [string] -or [string]::IsNullOrWhiteSpace($rpc.method)) { + throw 'invalid request' + } + if ($null -ne $rpc.id -and ($rpc.id -is [bool] -or ($rpc.id -isnot [string] -and $rpc.id -isnot [ValueType]))) { throw 'invalid id' } + } + catch { + Write-RpcFailure -IdText 'null' -Code -32600 -FailureCode 'invalid_request' -Message 'Expected one JSON-RPC 2.0 request or notification per line.' + continue + } + + $settings = Read-McpSettings + if ($settings.Code) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode $settings.Code -Message $settings.Message + continue + } + if (-not $settings.Enabled) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'server_disabled' -Message 'Enable MCP Server in Quicker settings.' + continue + } + if (-not $settings.TokenPresent) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'token_missing' -Message 'Quicker MCP has no token. Check MCP Server settings in Quicker.' + continue + } + + $request = $null + $response = $null + $mayHaveSent = $false + $forwardedResponse = $false + try { + # Only the persisted port is configurable. Never send the token to another host, + # an HTTP proxy, or a redirect target. + $request = [System.Net.HttpWebRequest]::Create('http://127.0.0.1:' + $settings.Port + '/mcp') + $request.Method = 'POST' + $request.Proxy = $null + $request.AllowAutoRedirect = $false + # Avoid automatic replay when a reused keep-alive socket is closed by the server. + $request.KeepAlive = $false + $request.Timeout = $RequestTimeoutSeconds * 1000 + $request.ReadWriteTimeout = $RequestTimeoutSeconds * 1000 + $request.ContentType = 'application/json; charset=utf-8' + $request.Accept = 'application/json, text/event-stream' + $request.Headers['Authorization'] = 'Bearer ' + $settings.Token + $request.Headers['Mcp-Client-Info'] = ($Client + '-quicker-plugin') + if ($protocolVersion) { $request.Headers['MCP-Protocol-Version'] = $protocolVersion } + $request.ServicePoint.Expect100Continue = $false + $bytes = $utf8.GetBytes($line) + $request.ContentLength = $bytes.Length + $mayHaveSent = $true + $stream = $request.GetRequestStream() + try { $stream.Write($bytes, 0, $bytes.Length) } + finally { $stream.Dispose() } + try { $response = $request.GetResponse() } + catch [System.Net.WebException] { + if ($null -eq $_.Exception.Response) { throw } + $response = $_.Exception.Response + } + $status = [int]$response.StatusCode + if ($status -lt 200 -or $status -ge 300) { + $failureCode = 'http_error' + $message = 'Quicker MCP rejected the HTTP request. Check Quicker before retrying.' + $stateUnknown = $status -ge 500 + if ($status -eq 401) { $failureCode = 'unauthorized'; $message = 'Quicker rejected the MCP token. Check MCP settings in Quicker.' } + elseif ($status -eq 403) { $failureCode = 'client_not_approved'; $message = 'Approve the connecting MCP client in Quicker, then reconnect.' } + elseif ($status -ge 300 -and $status -lt 400) { $failureCode = 'redirect_refused'; $message = 'Quicker MCP returned a redirect. Redirects are refused; check the local endpoint.' } + Write-RpcFailure -IdText $idText -Code -32003 -FailureCode $failureCode -Message $message -StateUnknown $stateUnknown -HttpStatus $status + continue + } + if ($status -eq 202) { + if ($idText) { throw 'missing response' } + continue + } + $reader = New-Object System.IO.StreamReader($response.GetResponseStream(), $utf8) + try { $body = $reader.ReadToEnd() } + finally { $reader.Dispose() } + if ([string]::IsNullOrWhiteSpace($body)) { + if ($idText) { throw 'missing response' } + continue + } + $payloads = New-Object 'System.Collections.Generic.List[string]' + if ($response.ContentType -match '^text/event-stream(?:;|$)') { + $eventData = New-Object 'System.Collections.Generic.List[string]' + foreach ($eventLine in ($body -split '\r\n|\n|\r')) { + if ($eventLine.Length -eq 0) { + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")); $eventData.Clear() } + } + elseif ($eventLine.StartsWith('data:')) { + $value = $eventLine.Substring(5) + if ($value.StartsWith(' ')) { $value = $value.Substring(1) } + $eventData.Add($value) + } + } + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")) } + } + elseif ($response.ContentType -match '^application/json(?:;|$)') { $payloads.Add($body) } + else { throw 'unsupported response' } + + foreach ($payload in $payloads) { + $parsed = $jsonReader.DeserializeObject($payload) + if ($null -eq $parsed -or $parsed -is [array] -or $parsed.jsonrpc -cne '2.0') { throw 'invalid response' } + $responseId = Get-RpcIdText $payload + if ($null -ne $responseId) { + if (-not $idText -or $forwardedResponse -or $parsed.id -cne $rpc.id) { throw 'unexpected response id' } + if ($rpc.method -ceq 'initialize' -and $parsed.result.protocolVersion -is [string]) { + $version = $parsed.result.protocolVersion + if ($version -match '^\d{4}-\d{2}-\d{2}$') { $protocolVersion = $version } + } + $forwardedResponse = $true + } + elseif ($parsed.method -isnot [string]) { throw 'invalid notification' } + Write-ProtocolLine (Compress-JsonText $payload) + } + if ($idText -and -not $forwardedResponse) { throw 'missing response' } + } + catch { + if (-not $forwardedResponse) { + Write-RpcFailure -IdText $idText -Code -32002 -FailureCode 'transport_failed' -StateUnknown $mayHaveSent -Message 'The Quicker MCP request did not produce a complete response. Its outcome may be unknown; inspect Quicker and reopen or reread the action before retrying a write.' + } + else { [Console]::Error.WriteLine('Quicker MCP: invalid data after the completed response.') } + } + finally { + if ($null -ne $response) { $response.Dispose() } + if ($null -ne $request) { $request.Abort() } + } +} diff --git a/plugins/quicker-cursor/skills/write-action/SKILL.md b/plugins/quicker-cursor/skills/write-action/SKILL.md new file mode 100644 index 0000000..6900424 --- /dev/null +++ b/plugins/quicker-cursor/skills/write-action/SKILL.md @@ -0,0 +1,28 @@ +--- +name: write-action +description: Create, edit, save and preview Windows automation actions in a running Quicker through its MCP tools. Use when the user asks to write or modify a Quicker action, or consult Quicker step knowledge. Does not apply to developing the Quicker application's source code. +--- + +# Write a Quicker action + +Use the Quicker plugin's MCP tools, whose names may have a client namespace prefix. These edit Quicker's virtual workspace, not the agent repository. In particular, use Quicker's read_file, write_file, edit_file and grep for slots and @knowledge; ordinary local filesystem tools cannot resolve them. + +The live server's initialize instructions and returned capability rules describe the current authoring protocol. Read all content blocks, including attachedRules. Tool availability and current knowledge are authoritative; this plugin does not carry a duplicate module catalog. + +## Authoring workflow + +1. Load the runtime grammar using skill_load with `{ "id": "action-source" }` before editing. For an unfamiliar step, follow its steps skill: grep one keyword under @knowledge/catalog, then read the matching module YAML. Use quicker_eval with steps.resolve only for the needed branch or referenced docs. Do not invent step names or input/output keys. Host JS uses JavaScript; action `$=` expressions use Quicker's C# expression syntax. +2. For a new action use quicker_create with a concise title. To edit an existing one, use quicker_search once, identify the intended result, then quicker_open with its actionId. Opening a formal action creates an editing draft; it does not overwrite the original. +3. Keep the returned slot and actionId. Always address files explicitly as `/program.source.yaml`, `/info.yaml` or `/files/...`, and save with quicker_save `{ "path": "" }`. Several tasks can share an agent backend process and Quicker's current location; do not rely on the implicit working directory or concurrently edit the same slot. +4. Write the program in program.source.yaml; use info.yaml for title, description and appearance. data.json and info.json are Host internals. quicker_save validates and stores the draft in Quicker's 暂存区. Inspect its result and correct reported errors before reporting completion. +5. Use quicker_preview `{ "target": "" }` when showing the result helps the user. It opens Quicker's designer. Only open a browser URL actually returned by the tool; do not construct one from a designer route. + +A request to write an action normally ends with a saved draft. Promote a new draft only when the user has specified an unambiguous existing destination scene; otherwise explain the 暂存区 → 保留到… flow. Overwrite a formal action or run an action only within the user's requested scope, using quicker_overwrite or quicker_run and Quicker's approval flow. State whether the action was only saved, also previewed, or actually run. + +## Failure handling + +Check JSON-RPC errors, MCP isError, `{ok:false,code,message}`, and quicker_eval's `{error:...}` envelope. Do not infer success from receiving text. + +When a mutation times out, the connection drops, or stateUnknown is returned, inspect the relevant slot/action before retrying. If create reports programCreated with an actionId, reopen that action rather than creating another. For catalog_stale or catalog_freshness_unknown, inspect the current action and preserve pending changes; do not blindly overwrite or re-open over dirty work. + +If tools are unavailable, consult [connection.md](references/connection.md). A missing write tool usually means Quicker's 允许 MCP 写入 switch is off. The plugin must not edit server.json or clients.json to enable access, grant consent, or change approval settings. diff --git a/plugins/quicker-cursor/skills/write-action/references/connection.md b/plugins/quicker-cursor/skills/write-action/references/connection.md new file mode 100644 index 0000000..04ed0f4 --- /dev/null +++ b/plugins/quicker-cursor/skills/write-action/references/connection.md @@ -0,0 +1,18 @@ +# Connection troubleshooting + +Requires Windows PowerShell 5.1 and a Quicker build with Settings → Agent → Enable MCP. Both supported Debug and Release builds work; older releases without that setting do not. + +Run powershell.exe -NoProfile -ExecutionPolicy Bypass -File "/scripts/quicker-mcp.ps1" -Check for a redacted configuration/TCP check. This does not prove client consent or write access. + +The relay reads the current port and token from the local Quicker configuration for each request. Keep the token out of commands, client configuration, logs and conversation. It uses only loopback and does not follow proxies or redirects. + +- Missing/disabled MCP: open Quicker Settings → Agent and enable MCP. +- Connection refused: start the supported Quicker build and check the configured port. +- HTTP 401: check the running instance; token rotation is picked up on the next read. +- HTTP 403 or a first connection waiting: complete Quicker's consent UI for the actual connecting client. The relay never grants itself access. +- Write tools absent: enable 允许 MCP 写入 in Quicker, keep the existing approval mode, then reconnect or start a new agent task. +- A write timeout can leave an unknown outcome: inspect the slot before retrying. + +After installation/update, reload the client and create a new task. Cursor IDE: Developer: Reload Window; Cursor CLI: restart agent. Claude Code: /reload-plugins or restart. VS Code: MCP: List Servers → Quicker → Restart. Gemini CLI: restart. + +Plugins ship their own relay. Cursor and Claude expand their respective plugin-root variables; Codex uses package-relative cwd. Generic MCP configuration points at the installed copy, never the development checkout. Do not edit server.json or clients.json to grant access. diff --git a/plugins/quicker-mcp/README.md b/plugins/quicker-mcp/README.md new file mode 100644 index 0000000..992ed4e --- /dev/null +++ b/plugins/quicker-mcp/README.md @@ -0,0 +1,9 @@ +# Quicker mcp 接入包 + +Windows PowerShell 5.1 + 支持 MCP 的本机 Quicker。此目录是完整运行单元,不依赖开发检出。 + +安装、更新、卸载和实测兼容状态见 [公开仓库](https://github.com/QuickerOrg/quicker-agent-integrations#readme) 与 [客户端安装](https://github.com/QuickerOrg/quicker-agent-integrations/blob/main/docs/客户端安装.md)。 + +启用 Quicker 设置 → Agent 中的 MCP 与允许写入,然后重新加载客户端并完成 Quicker 客户端授权。插件读取实时动作知识,默认把结果保存到暂存区;安装本身不授予执行或覆盖权限。运行时不需要 Git、Python 或 Node。 + +传输脚本与技能由 shared/ 生成;维护时运行 scripts/sync-packages.py,不手工修改副本。 diff --git a/plugins/quicker-mcp/package.json b/plugins/quicker-mcp/package.json new file mode 100644 index 0000000..53dfe21 --- /dev/null +++ b/plugins/quicker-mcp/package.json @@ -0,0 +1,6 @@ +{ + "name": "quicker-mcp", + "version": "0.2.0", + "private": true, + "description": "通过本机 Quicker MCP 编写、保存和预览自动化动作。" +} diff --git a/plugins/quicker-mcp/scripts/quicker-mcp.ps1 b/plugins/quicker-mcp/scripts/quicker-mcp.ps1 new file mode 100644 index 0000000..3811fe8 --- /dev/null +++ b/plugins/quicker-mcp/scripts/quicker-mcp.ps1 @@ -0,0 +1,285 @@ +[CmdletBinding()] +param( + [string]$SettingsPath = '', + [ValidateRange(1, 600)] + [int]$RequestTimeoutSeconds = 180, + [ValidateSet('codex', 'cursor', 'claude', 'vscode', 'gemini')] + [string]$Client = 'codex', + [switch]$Check +) + +# A transport adapter only: Quicker owns tools, authoring state and approval policy. +# Keep JSON payloads as text so Windows PowerShell never rewrites schemas or numbers. +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$utf8 = New-Object System.Text.UTF8Encoding($false) +[Console]::InputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 +$OutputEncoding = $utf8 +Add-Type -AssemblyName System.Web.Extensions +$jsonReader = New-Object System.Web.Script.Serialization.JavaScriptSerializer +$jsonReader.MaxJsonLength = [int]::MaxValue +$jsonReader.RecursionLimit = 256 +if ([string]::IsNullOrWhiteSpace($SettingsPath)) { + $SettingsPath = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.quicker\mcp\server.json' +} + +function Write-ProtocolLine([string]$Text) { + [Console]::Out.WriteLine($Text) + [Console]::Out.Flush() +} + +function Compress-JsonText([string]$Text) { + $builder = New-Object System.Text.StringBuilder + $inString = $false + $escaped = $false + foreach ($character in $Text.ToCharArray()) { + if ($inString) { + [void]$builder.Append($character) + if ($escaped) { $escaped = $false } + elseif ($character -eq '\') { $escaped = $true } + elseif ($character -eq '"') { $inString = $false } + } + elseif ($character -eq '"') { + $inString = $true + [void]$builder.Append($character) + } + elseif (-not [char]::IsWhiteSpace($character)) { [void]$builder.Append($character) } + } + return $builder.ToString() +} + +function Get-RpcIdText([string]$Text) { + # Read only the top-level id token, preserving a numeric id's exact spelling. + $depth = 0 + for ($index = 0; $index -lt $Text.Length; $index++) { + $character = $Text[$index] + if ($character -eq '{' -or $character -eq '[') { $depth++; continue } + if ($character -eq '}' -or $character -eq ']') { $depth--; continue } + if ($character -ne '"') { continue } + $start = $index + $index++ + for (; $index -lt $Text.Length; $index++) { + if ($Text[$index] -eq '\') { $index++; continue } + if ($Text[$index] -eq '"') { break } + } + if ($depth -ne 1) { continue } + $after = $index + 1 + while ($after -lt $Text.Length -and [char]::IsWhiteSpace($Text[$after])) { $after++ } + if ($after -ge $Text.Length -or $Text[$after] -ne ':') { continue } + $keyToken = $Text.Substring($start, $index - $start + 1) + $key = $jsonReader.DeserializeObject('{"key":' + $keyToken + '}') + if ($key.key -cne 'id') { continue } + $valueStart = $after + 1 + while ($valueStart -lt $Text.Length -and [char]::IsWhiteSpace($Text[$valueStart])) { $valueStart++ } + $valueEnd = $valueStart + if ($Text[$valueStart] -eq '"') { + $valueEnd++ + for (; $valueEnd -lt $Text.Length; $valueEnd++) { + if ($Text[$valueEnd] -eq '\') { $valueEnd++; continue } + if ($Text[$valueEnd] -eq '"') { $valueEnd++; break } + } + } + else { + while ($valueEnd -lt $Text.Length -and $Text[$valueEnd] -ne ',' -and $Text[$valueEnd] -ne '}') { $valueEnd++ } + } + return $Text.Substring($valueStart, $valueEnd - $valueStart).Trim() + } + return $null +} + +function Write-RpcFailure { + param([string]$IdText, [int]$Code, [string]$FailureCode, [string]$Message, + [bool]$StateUnknown = $false, [int]$HttpStatus = 0) + if ([string]::IsNullOrEmpty($IdText)) { + # Notifications must not receive JSON-RPC replies. Diagnostics never contain input data. + [Console]::Error.WriteLine('Quicker MCP: ' + $FailureCode + '. ' + $Message) + return + } + $data = [ordered]@{ code = $FailureCode; stateUnknown = $StateUnknown } + if ($HttpStatus -ne 0) { $data.httpStatus = $HttpStatus } + $errorBody = [ordered]@{ code = $Code; message = $Message; data = $data } | ConvertTo-Json -Depth 8 -Compress + Write-ProtocolLine ('{"jsonrpc":"2.0","id":' + $IdText + ',"error":' + $errorBody + '}') +} + +function Read-McpSettings { + if (-not [System.IO.File]::Exists($SettingsPath)) { + return @{ Code = 'settings_missing'; Message = 'Enable MCP Server in a Debug build of Quicker first.' } + } + try { + $settings = $jsonReader.DeserializeObject([System.IO.File]::ReadAllText($SettingsPath, $utf8)) + if ($null -eq $settings -or $settings -is [array]) { throw 'invalid' } + $port = 0 + if (-not [int]::TryParse([string]$settings.Port, [ref]$port) -or $port -lt 1024 -or $port -gt 65535) { + throw 'invalid' + } + $tokenPresent = $settings.Token -is [string] -and -not [string]::IsNullOrWhiteSpace($settings.Token) + if ($tokenPresent -and $settings.Token -match '\s') { throw 'invalid' } + $enabled = $settings.Enabled -is [bool] -and $settings.Enabled + $allowWrites = $settings.AllowWrites -is [bool] -and $settings.AllowWrites + return @{ Enabled = $enabled; AllowWrites = $allowWrites; Port = $port; + TokenPresent = $tokenPresent; Token = $settings.Token } + } + catch { + return @{ Code = 'settings_invalid'; Message = 'Quicker MCP settings could not be read. Check them in Quicker.' } + } +} + +if ($Check) { + $settings = Read-McpSettings + if ($settings.Code) { + Write-ProtocolLine (([ordered]@{ ok = $false; code = $settings.Code; message = $settings.Message }) | ConvertTo-Json -Compress) + exit 1 + } + $reachable = $false + $tcpClient = New-Object System.Net.Sockets.TcpClient + try { + $pending = $tcpClient.BeginConnect('127.0.0.1', $settings.Port, $null, $null) + if ($pending.AsyncWaitHandle.WaitOne(2000)) { + $tcpClient.EndConnect($pending) + $reachable = $tcpClient.Connected + } + $pending.AsyncWaitHandle.Close() + } + catch { $reachable = $false } + finally { $tcpClient.Close() } + $ready = $settings.Enabled -and $settings.TokenPresent -and $reachable + Write-ProtocolLine (([ordered]@{ ok = [bool]$ready; enabled = [bool]$settings.Enabled; + allowWrites = [bool]$settings.AllowWrites; port = $settings.Port; + tokenPresent = [bool]$settings.TokenPresent; portReachable = $reachable }) | ConvertTo-Json -Compress) + if ($ready) { exit 0 } + exit 1 +} + +$protocolVersion = $null +while ($null -ne ($line = [Console]::In.ReadLine())) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $idText = $null + try { + $rpc = $jsonReader.DeserializeObject($line) + $idText = Get-RpcIdText $line + if ($null -eq $rpc -or $rpc -is [array] -or $rpc.jsonrpc -cne '2.0' -or + $rpc.method -isnot [string] -or [string]::IsNullOrWhiteSpace($rpc.method)) { + throw 'invalid request' + } + if ($null -ne $rpc.id -and ($rpc.id -is [bool] -or ($rpc.id -isnot [string] -and $rpc.id -isnot [ValueType]))) { throw 'invalid id' } + } + catch { + Write-RpcFailure -IdText 'null' -Code -32600 -FailureCode 'invalid_request' -Message 'Expected one JSON-RPC 2.0 request or notification per line.' + continue + } + + $settings = Read-McpSettings + if ($settings.Code) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode $settings.Code -Message $settings.Message + continue + } + if (-not $settings.Enabled) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'server_disabled' -Message 'Enable MCP Server in Quicker settings.' + continue + } + if (-not $settings.TokenPresent) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'token_missing' -Message 'Quicker MCP has no token. Check MCP Server settings in Quicker.' + continue + } + + $request = $null + $response = $null + $mayHaveSent = $false + $forwardedResponse = $false + try { + # Only the persisted port is configurable. Never send the token to another host, + # an HTTP proxy, or a redirect target. + $request = [System.Net.HttpWebRequest]::Create('http://127.0.0.1:' + $settings.Port + '/mcp') + $request.Method = 'POST' + $request.Proxy = $null + $request.AllowAutoRedirect = $false + # Avoid automatic replay when a reused keep-alive socket is closed by the server. + $request.KeepAlive = $false + $request.Timeout = $RequestTimeoutSeconds * 1000 + $request.ReadWriteTimeout = $RequestTimeoutSeconds * 1000 + $request.ContentType = 'application/json; charset=utf-8' + $request.Accept = 'application/json, text/event-stream' + $request.Headers['Authorization'] = 'Bearer ' + $settings.Token + $request.Headers['Mcp-Client-Info'] = ($Client + '-quicker-plugin') + if ($protocolVersion) { $request.Headers['MCP-Protocol-Version'] = $protocolVersion } + $request.ServicePoint.Expect100Continue = $false + $bytes = $utf8.GetBytes($line) + $request.ContentLength = $bytes.Length + $mayHaveSent = $true + $stream = $request.GetRequestStream() + try { $stream.Write($bytes, 0, $bytes.Length) } + finally { $stream.Dispose() } + try { $response = $request.GetResponse() } + catch [System.Net.WebException] { + if ($null -eq $_.Exception.Response) { throw } + $response = $_.Exception.Response + } + $status = [int]$response.StatusCode + if ($status -lt 200 -or $status -ge 300) { + $failureCode = 'http_error' + $message = 'Quicker MCP rejected the HTTP request. Check Quicker before retrying.' + $stateUnknown = $status -ge 500 + if ($status -eq 401) { $failureCode = 'unauthorized'; $message = 'Quicker rejected the MCP token. Check MCP settings in Quicker.' } + elseif ($status -eq 403) { $failureCode = 'client_not_approved'; $message = 'Approve the connecting MCP client in Quicker, then reconnect.' } + elseif ($status -ge 300 -and $status -lt 400) { $failureCode = 'redirect_refused'; $message = 'Quicker MCP returned a redirect. Redirects are refused; check the local endpoint.' } + Write-RpcFailure -IdText $idText -Code -32003 -FailureCode $failureCode -Message $message -StateUnknown $stateUnknown -HttpStatus $status + continue + } + if ($status -eq 202) { + if ($idText) { throw 'missing response' } + continue + } + $reader = New-Object System.IO.StreamReader($response.GetResponseStream(), $utf8) + try { $body = $reader.ReadToEnd() } + finally { $reader.Dispose() } + if ([string]::IsNullOrWhiteSpace($body)) { + if ($idText) { throw 'missing response' } + continue + } + $payloads = New-Object 'System.Collections.Generic.List[string]' + if ($response.ContentType -match '^text/event-stream(?:;|$)') { + $eventData = New-Object 'System.Collections.Generic.List[string]' + foreach ($eventLine in ($body -split '\r\n|\n|\r')) { + if ($eventLine.Length -eq 0) { + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")); $eventData.Clear() } + } + elseif ($eventLine.StartsWith('data:')) { + $value = $eventLine.Substring(5) + if ($value.StartsWith(' ')) { $value = $value.Substring(1) } + $eventData.Add($value) + } + } + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")) } + } + elseif ($response.ContentType -match '^application/json(?:;|$)') { $payloads.Add($body) } + else { throw 'unsupported response' } + + foreach ($payload in $payloads) { + $parsed = $jsonReader.DeserializeObject($payload) + if ($null -eq $parsed -or $parsed -is [array] -or $parsed.jsonrpc -cne '2.0') { throw 'invalid response' } + $responseId = Get-RpcIdText $payload + if ($null -ne $responseId) { + if (-not $idText -or $forwardedResponse -or $parsed.id -cne $rpc.id) { throw 'unexpected response id' } + if ($rpc.method -ceq 'initialize' -and $parsed.result.protocolVersion -is [string]) { + $version = $parsed.result.protocolVersion + if ($version -match '^\d{4}-\d{2}-\d{2}$') { $protocolVersion = $version } + } + $forwardedResponse = $true + } + elseif ($parsed.method -isnot [string]) { throw 'invalid notification' } + Write-ProtocolLine (Compress-JsonText $payload) + } + if ($idText -and -not $forwardedResponse) { throw 'missing response' } + } + catch { + if (-not $forwardedResponse) { + Write-RpcFailure -IdText $idText -Code -32002 -FailureCode 'transport_failed' -StateUnknown $mayHaveSent -Message 'The Quicker MCP request did not produce a complete response. Its outcome may be unknown; inspect Quicker and reopen or reread the action before retrying a write.' + } + else { [Console]::Error.WriteLine('Quicker MCP: invalid data after the completed response.') } + } + finally { + if ($null -ne $response) { $response.Dispose() } + if ($null -ne $request) { $request.Abort() } + } +} diff --git a/plugins/quicker-mcp/skills/write-action/SKILL.md b/plugins/quicker-mcp/skills/write-action/SKILL.md new file mode 100644 index 0000000..6900424 --- /dev/null +++ b/plugins/quicker-mcp/skills/write-action/SKILL.md @@ -0,0 +1,28 @@ +--- +name: write-action +description: Create, edit, save and preview Windows automation actions in a running Quicker through its MCP tools. Use when the user asks to write or modify a Quicker action, or consult Quicker step knowledge. Does not apply to developing the Quicker application's source code. +--- + +# Write a Quicker action + +Use the Quicker plugin's MCP tools, whose names may have a client namespace prefix. These edit Quicker's virtual workspace, not the agent repository. In particular, use Quicker's read_file, write_file, edit_file and grep for slots and @knowledge; ordinary local filesystem tools cannot resolve them. + +The live server's initialize instructions and returned capability rules describe the current authoring protocol. Read all content blocks, including attachedRules. Tool availability and current knowledge are authoritative; this plugin does not carry a duplicate module catalog. + +## Authoring workflow + +1. Load the runtime grammar using skill_load with `{ "id": "action-source" }` before editing. For an unfamiliar step, follow its steps skill: grep one keyword under @knowledge/catalog, then read the matching module YAML. Use quicker_eval with steps.resolve only for the needed branch or referenced docs. Do not invent step names or input/output keys. Host JS uses JavaScript; action `$=` expressions use Quicker's C# expression syntax. +2. For a new action use quicker_create with a concise title. To edit an existing one, use quicker_search once, identify the intended result, then quicker_open with its actionId. Opening a formal action creates an editing draft; it does not overwrite the original. +3. Keep the returned slot and actionId. Always address files explicitly as `/program.source.yaml`, `/info.yaml` or `/files/...`, and save with quicker_save `{ "path": "" }`. Several tasks can share an agent backend process and Quicker's current location; do not rely on the implicit working directory or concurrently edit the same slot. +4. Write the program in program.source.yaml; use info.yaml for title, description and appearance. data.json and info.json are Host internals. quicker_save validates and stores the draft in Quicker's 暂存区. Inspect its result and correct reported errors before reporting completion. +5. Use quicker_preview `{ "target": "" }` when showing the result helps the user. It opens Quicker's designer. Only open a browser URL actually returned by the tool; do not construct one from a designer route. + +A request to write an action normally ends with a saved draft. Promote a new draft only when the user has specified an unambiguous existing destination scene; otherwise explain the 暂存区 → 保留到… flow. Overwrite a formal action or run an action only within the user's requested scope, using quicker_overwrite or quicker_run and Quicker's approval flow. State whether the action was only saved, also previewed, or actually run. + +## Failure handling + +Check JSON-RPC errors, MCP isError, `{ok:false,code,message}`, and quicker_eval's `{error:...}` envelope. Do not infer success from receiving text. + +When a mutation times out, the connection drops, or stateUnknown is returned, inspect the relevant slot/action before retrying. If create reports programCreated with an actionId, reopen that action rather than creating another. For catalog_stale or catalog_freshness_unknown, inspect the current action and preserve pending changes; do not blindly overwrite or re-open over dirty work. + +If tools are unavailable, consult [connection.md](references/connection.md). A missing write tool usually means Quicker's 允许 MCP 写入 switch is off. The plugin must not edit server.json or clients.json to enable access, grant consent, or change approval settings. diff --git a/plugins/quicker-mcp/skills/write-action/references/connection.md b/plugins/quicker-mcp/skills/write-action/references/connection.md new file mode 100644 index 0000000..04ed0f4 --- /dev/null +++ b/plugins/quicker-mcp/skills/write-action/references/connection.md @@ -0,0 +1,18 @@ +# Connection troubleshooting + +Requires Windows PowerShell 5.1 and a Quicker build with Settings → Agent → Enable MCP. Both supported Debug and Release builds work; older releases without that setting do not. + +Run powershell.exe -NoProfile -ExecutionPolicy Bypass -File "/scripts/quicker-mcp.ps1" -Check for a redacted configuration/TCP check. This does not prove client consent or write access. + +The relay reads the current port and token from the local Quicker configuration for each request. Keep the token out of commands, client configuration, logs and conversation. It uses only loopback and does not follow proxies or redirects. + +- Missing/disabled MCP: open Quicker Settings → Agent and enable MCP. +- Connection refused: start the supported Quicker build and check the configured port. +- HTTP 401: check the running instance; token rotation is picked up on the next read. +- HTTP 403 or a first connection waiting: complete Quicker's consent UI for the actual connecting client. The relay never grants itself access. +- Write tools absent: enable 允许 MCP 写入 in Quicker, keep the existing approval mode, then reconnect or start a new agent task. +- A write timeout can leave an unknown outcome: inspect the slot before retrying. + +After installation/update, reload the client and create a new task. Cursor IDE: Developer: Reload Window; Cursor CLI: restart agent. Claude Code: /reload-plugins or restart. VS Code: MCP: List Servers → Quicker → Restart. Gemini CLI: restart. + +Plugins ship their own relay. Cursor and Claude expand their respective plugin-root variables; Codex uses package-relative cwd. Generic MCP configuration points at the installed copy, never the development checkout. Do not edit server.json or clients.json to grant access. diff --git a/plugins/quicker/.codex-plugin/plugin.json b/plugins/quicker/.codex-plugin/plugin.json index 97b038e..d803335 100644 --- a/plugins/quicker/.codex-plugin/plugin.json +++ b/plugins/quicker/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "quicker", - "version": "0.1.1", + "version": "0.2.0", "description": "通过本机 Quicker MCP 编写、修改、校验和预览自动化动作。", "author": { "name": "QuickerOrg" @@ -12,10 +12,13 @@ "interface": { "displayName": "Quicker 动作助手", "shortDescription": "用自然语言编写 Quicker 动作,保存到暂存区并预览。", - "longDescription": "连接本机运行中的 Quicker Debug 版,查询实时步骤知识、编写和修改动作、校验保存草稿,并在 Quicker 中预览。需要在 Quicker 设置中启用 MCP 和允许写入,首次连接需在 Quicker 授权。", + "longDescription": "连接本机运行且支持 MCP 的 Quicker 构建,查询实时步骤知识、编写和修改动作、校验保存草稿,并在 Quicker 中预览。需要在 Quicker 设置中启用 MCP 和允许写入,首次连接需在 Quicker 授权。", "developerName": "QuickerOrg", "category": "Productivity", - "capabilities": ["Read", "Write"], + "capabilities": [ + "Read", + "Write" + ], "defaultPrompt": [ "用 Quicker 写一个动作,显示“来自 Codex”,保存到暂存区并打开预览。", "帮我修改一个已有的 Quicker 动作,先保存为草稿。", diff --git a/plugins/quicker/scripts/quicker-mcp.ps1 b/plugins/quicker/scripts/quicker-mcp.ps1 index 2dd3775..3811fe8 100644 --- a/plugins/quicker/scripts/quicker-mcp.ps1 +++ b/plugins/quicker/scripts/quicker-mcp.ps1 @@ -3,6 +3,8 @@ param( [string]$SettingsPath = '', [ValidateRange(1, 600)] [int]$RequestTimeoutSeconds = 180, + [ValidateSet('codex', 'cursor', 'claude', 'vscode', 'gemini')] + [string]$Client = 'codex', [switch]$Check ) @@ -130,17 +132,17 @@ if ($Check) { exit 1 } $reachable = $false - $client = New-Object System.Net.Sockets.TcpClient + $tcpClient = New-Object System.Net.Sockets.TcpClient try { - $pending = $client.BeginConnect('127.0.0.1', $settings.Port, $null, $null) + $pending = $tcpClient.BeginConnect('127.0.0.1', $settings.Port, $null, $null) if ($pending.AsyncWaitHandle.WaitOne(2000)) { - $client.EndConnect($pending) - $reachable = $client.Connected + $tcpClient.EndConnect($pending) + $reachable = $tcpClient.Connected } $pending.AsyncWaitHandle.Close() } catch { $reachable = $false } - finally { $client.Close() } + finally { $tcpClient.Close() } $ready = $settings.Enabled -and $settings.TokenPresent -and $reachable Write-ProtocolLine (([ordered]@{ ok = [bool]$ready; enabled = [bool]$settings.Enabled; allowWrites = [bool]$settings.AllowWrites; port = $settings.Port; @@ -199,7 +201,7 @@ while ($null -ne ($line = [Console]::In.ReadLine())) { $request.ContentType = 'application/json; charset=utf-8' $request.Accept = 'application/json, text/event-stream' $request.Headers['Authorization'] = 'Bearer ' + $settings.Token - $request.Headers['Mcp-Client-Info'] = 'codex-quicker-plugin' + $request.Headers['Mcp-Client-Info'] = ($Client + '-quicker-plugin') if ($protocolVersion) { $request.Headers['MCP-Protocol-Version'] = $protocolVersion } $request.ServicePoint.Expect100Continue = $false $bytes = $utf8.GetBytes($line) @@ -219,7 +221,7 @@ while ($null -ne ($line = [Console]::In.ReadLine())) { $message = 'Quicker MCP rejected the HTTP request. Check Quicker before retrying.' $stateUnknown = $status -ge 500 if ($status -eq 401) { $failureCode = 'unauthorized'; $message = 'Quicker rejected the MCP token. Check MCP settings in Quicker.' } - elseif ($status -eq 403) { $failureCode = 'client_not_approved'; $message = 'Approve the Codex MCP client in Quicker, then reconnect.' } + elseif ($status -eq 403) { $failureCode = 'client_not_approved'; $message = 'Approve the connecting MCP client in Quicker, then reconnect.' } elseif ($status -ge 300 -and $status -lt 400) { $failureCode = 'redirect_refused'; $message = 'Quicker MCP returned a redirect. Redirects are refused; check the local endpoint.' } Write-RpcFailure -IdText $idText -Code -32003 -FailureCode $failureCode -Message $message -StateUnknown $stateUnknown -HttpStatus $status continue diff --git a/plugins/quicker/skills/write-action/SKILL.md b/plugins/quicker/skills/write-action/SKILL.md index d908f7f..6900424 100644 --- a/plugins/quicker/skills/write-action/SKILL.md +++ b/plugins/quicker/skills/write-action/SKILL.md @@ -5,7 +5,7 @@ description: Create, edit, save and preview Windows automation actions in a runn # Write a Quicker action -Use the Quicker plugin's MCP tools, whose names may have a client namespace prefix. These edit Quicker's virtual workspace, not the Codex repository. In particular, use Quicker's read_file, write_file, edit_file and grep for slots and @knowledge; ordinary local filesystem tools cannot resolve them. +Use the Quicker plugin's MCP tools, whose names may have a client namespace prefix. These edit Quicker's virtual workspace, not the agent repository. In particular, use Quicker's read_file, write_file, edit_file and grep for slots and @knowledge; ordinary local filesystem tools cannot resolve them. The live server's initialize instructions and returned capability rules describe the current authoring protocol. Read all content blocks, including attachedRules. Tool availability and current knowledge are authoritative; this plugin does not carry a duplicate module catalog. @@ -13,7 +13,7 @@ The live server's initialize instructions and returned capability rules describe 1. Load the runtime grammar using skill_load with `{ "id": "action-source" }` before editing. For an unfamiliar step, follow its steps skill: grep one keyword under @knowledge/catalog, then read the matching module YAML. Use quicker_eval with steps.resolve only for the needed branch or referenced docs. Do not invent step names or input/output keys. Host JS uses JavaScript; action `$=` expressions use Quicker's C# expression syntax. 2. For a new action use quicker_create with a concise title. To edit an existing one, use quicker_search once, identify the intended result, then quicker_open with its actionId. Opening a formal action creates an editing draft; it does not overwrite the original. -3. Keep the returned slot and actionId. Always address files explicitly as `/program.source.yaml`, `/info.yaml` or `/files/...`, and save with quicker_save `{ "path": "" }`. Several tasks can share a Codex backend process and Quicker's current location; do not rely on the implicit working directory or concurrently edit the same slot. +3. Keep the returned slot and actionId. Always address files explicitly as `/program.source.yaml`, `/info.yaml` or `/files/...`, and save with quicker_save `{ "path": "" }`. Several tasks can share an agent backend process and Quicker's current location; do not rely on the implicit working directory or concurrently edit the same slot. 4. Write the program in program.source.yaml; use info.yaml for title, description and appearance. data.json and info.json are Host internals. quicker_save validates and stores the draft in Quicker's 暂存区. Inspect its result and correct reported errors before reporting completion. 5. Use quicker_preview `{ "target": "" }` when showing the result helps the user. It opens Quicker's designer. Only open a browser URL actually returned by the tool; do not construct one from a designer route. diff --git a/plugins/quicker/skills/write-action/references/connection.md b/plugins/quicker/skills/write-action/references/connection.md index 8e8ccf6..04ed0f4 100644 --- a/plugins/quicker/skills/write-action/references/connection.md +++ b/plugins/quicker/skills/write-action/references/connection.md @@ -1,27 +1,18 @@ # Connection troubleshooting -This plugin runs on Windows with Windows PowerShell 5.1. Its local stdio relay forwards the running Quicker Debug application's existing Streamable HTTP MCP interface. It does not start Quicker or provide an alternate action engine. +Requires Windows PowerShell 5.1 and a Quicker build with Settings → Agent → Enable MCP. Both supported Debug and Release builds work; older releases without that setting do not. -Run the bundled diagnostic using the actual installed plugin root: +Run powershell.exe -NoProfile -ExecutionPolicy Bypass -File "/scripts/quicker-mcp.ps1" -Check for a redacted configuration/TCP check. This does not prove client consent or write access. -```powershell -Push-Location -LiteralPath "" -try { - powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File .\scripts\quicker-mcp.ps1 -Check -} finally { - Pop-Location -} -``` +The relay reads the current port and token from the local Quicker configuration for each request. Keep the token out of commands, client configuration, logs and conversation. It uses only loopback and does not follow proxies or redirects. -Use the installed directory reported by Codex, not a literal `${PLUGIN_ROOT}` argument. This plugin uses a package-relative working directory because the Codex legacy MCP loader does not expand that placeholder in arguments. +- Missing/disabled MCP: open Quicker Settings → Agent and enable MCP. +- Connection refused: start the supported Quicker build and check the configured port. +- HTTP 401: check the running instance; token rotation is picked up on the next read. +- HTTP 403 or a first connection waiting: complete Quicker's consent UI for the actual connecting client. The relay never grants itself access. +- Write tools absent: enable 允许 MCP 写入 in Quicker, keep the existing approval mode, then reconnect or start a new agent task. +- A write timeout can leave an unknown outcome: inspect the slot before retrying. -The relay reads `%USERPROFILE%/.quicker/mcp/server.json` for the port and bearer token on each request. It connects only to 127.0.0.1, does not follow redirects or use an HTTP proxy, and never writes the token into plugin files. Do not print the configuration file, token, HTTP authorization header, or clients.json in tool results or the conversation. +After installation/update, reload the client and create a new task. Cursor IDE: Developer: Reload Window; Cursor CLI: restart agent. Claude Code: /reload-plugins or restart. VS Code: MCP: List Servers → Quicker → Restart. Gemini CLI: restart. -- Missing configuration or disabled MCP: use a Debug Quicker build and enable MCP in Quicker settings. Current Release builds have no MCP listener. -- Cannot connect: ensure that the Debug application is running and its configured MCP port is listening. -- HTTP 401: authentication was rejected. Retry a read after checking the currently running Quicker instance; token rotation is picked up on the next request. -- HTTP 403: Quicker denied client consent. The user must approve the intended client through Quicker's own UI. -- Read tools present, write tools absent: enable 允许 MCP 写入 in Quicker settings, then start a new Codex task to rediscover tools. Keep the existing Quicker approval mode. -- First connection waiting: check for Quicker's local client consent window. The plugin allows a bounded wait for consent; it does not auto-approve. - -After installing or updating this plugin, start a new Codex task to load its skills and tools. A task that started before installation may not have them. +Plugins ship their own relay. Cursor and Claude expand their respective plugin-root variables; Codex uses package-relative cwd. Generic MCP configuration points at the installed copy, never the development checkout. Do not edit server.json or clients.json to grant access. diff --git a/scripts/install-client.ps1 b/scripts/install-client.ps1 new file mode 100644 index 0000000..d28ac20 --- /dev/null +++ b/scripts/install-client.ps1 @@ -0,0 +1,160 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateSet('cursor', 'vscode', 'gemini')] + [string]$Client, + [string]$UserRoot = [Environment]::GetFolderPath('UserProfile'), + [switch]$Uninstall +) + +# Install only our package and MCP entry; credentials stay in Quicker. +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$userPath = [IO.Path]::GetFullPath($UserRoot) +$utf8 = New-Object Text.UTF8Encoding($false) +Add-Type -AssemblyName System.Web.Extensions +$json = New-Object Web.Script.Serialization.JavaScriptSerializer +$json.MaxJsonLength = [int]::MaxValue +$json.RecursionLimit = 256 + +function Get-PackageHash([string]$Path) { + $algorithm = [Security.Cryptography.SHA256]::Create() + $stream = [IO.File]::OpenRead($Path) + try { return [BitConverter]::ToString($algorithm.ComputeHash($stream)).Replace('-', '') } + finally { $stream.Dispose(); $algorithm.Dispose() } +} + +function Assert-UserPath([string]$Path) { + $full = [IO.Path]::GetFullPath($Path) + if (-not $full.StartsWith($userPath.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Install path escaped the selected user directory.' + } + $probe = $full + while ($probe -and $probe.Length -ge $userPath.Length) { + if (Test-Path -LiteralPath $probe) { + if ((Get-Item -LiteralPath $probe -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw 'Installation through a symlink or junction is not supported.' + } + } + $probe = Split-Path -Parent $probe + } + return $full +} + +function Read-Json([string]$Path) { + $value = $json.DeserializeObject([IO.File]::ReadAllText($Path)) + if ($value -isnot [System.Collections.IDictionary]) { throw "Expected JSON object: $Path" } + return ,$value +} + +function Write-Json([string]$Path, $Value) { + [void](Assert-UserPath $Path) + [void][IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) + $temporary = $Path + '.' + [Guid]::NewGuid().ToString('N') + '.tmp' + [IO.File]::WriteAllText($temporary, (ConvertTo-Json -InputObject $Value -Depth 100 -Compress), $utf8) + if (Test-Path -LiteralPath $Path) { + [IO.File]::Replace($temporary, $Path, [NullString]::Value) + } else { [IO.File]::Move($temporary, $Path) } +} + +$packageName = if ($Client -eq 'cursor') { 'quicker-cursor' } else { 'quicker-mcp' } +$relativeTarget = if ($Client -eq 'cursor') { '.cursor/plugins/local/quicker' } else { '.quicker/agent-integrations/' + $Client } +$target = Assert-UserPath (Join-Path $userPath $relativeTarget) +$markerPath = Join-Path $target '.quicker-managed.json' +$marker = $null +if (Test-Path -LiteralPath $target) { + if (-not (Test-Path -LiteralPath $markerPath)) { throw 'An unmanaged package already exists. It was not changed.' } + $marker = Read-Json $markerPath + if ($marker['owner'] -ne 'QuickerOrg/quicker-agent-integrations' -or $marker['client'] -ne $Client) { throw 'Package ownership does not match.' } + foreach ($directory in Get-ChildItem -LiteralPath $target -Recurse -Directory -Force) { + [void](Assert-UserPath $directory.FullName) + } + # Preserve local edits and reject unexpected files before replacing/removing a package. + foreach ($file in Get-ChildItem -LiteralPath $target -Recurse -File -Force) { + [void](Assert-UserPath $file.FullName) + $relative = $file.FullName.Substring($target.Length + 1).Replace('\', '/') + if ($relative -eq '.quicker-managed.json') { continue } + if (-not $marker['files'].ContainsKey($relative) -or (Get-PackageHash $file.FullName) -ne $marker['files'][$relative]) { + throw 'The installed package contains local changes. It was not changed.' + } + } +} + +$configPath = $null +$config = $null +$section = 'mcpServers' +$server = $null +if ($Client -ne 'cursor') { + $relativeConfig = if ($Client -eq 'vscode') { 'AppData/Roaming/Code/User/mcp.json' } else { '.gemini/settings.json' } + $configPath = Assert-UserPath (Join-Path $userPath $relativeConfig) + if ($Client -eq 'vscode') { $section = 'servers' } + $config = if (Test-Path -LiteralPath $configPath) { Read-Json $configPath } else { @{} } + if (-not $config.ContainsKey($section)) { $config[$section] = @{} } + if ($config[$section] -isnot [System.Collections.IDictionary]) { throw 'MCP server configuration must be an object.' } + $server = @{ + command = 'powershell.exe' + args = @('-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $target 'scripts/quicker-mcp.ps1'), '-Client', $Client) + } + if ($Client -eq 'vscode') { $server['type'] = 'stdio' } + if ($config[$section].ContainsKey('quicker')) { + $existing = $config[$section]['quicker'] + if (-not $marker -or (ConvertTo-Json -InputObject $existing -Depth 100 -Compress) -ne (ConvertTo-Json -InputObject $marker['server'] -Depth 100 -Compress)) { + throw 'The quicker MCP entry is not owned by this installer or was edited. It was not changed.' + } + } +} + +if ($Uninstall) { + if (-not $marker) { Write-Output 'Nothing installed by this installer.'; exit 0 } + if ($configPath) { + [void]$config[$section].Remove('quicker') + Write-Json $configPath $config + } + [void](Assert-UserPath $target) + Remove-Item -LiteralPath $target -Recurse -Force + Write-Output "Removed Quicker integration for $Client. Reload the client." + exit 0 +} + +$source = Join-Path $repo ('plugins/' + $packageName) +if (-not (Test-Path -LiteralPath (Join-Path $source 'scripts/quicker-mcp.ps1'))) { throw 'Incomplete source package.' } +foreach ($entry in Get-ChildItem -LiteralPath $source -Recurse -Force) { + if ($entry.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Source package must be self-contained.' } +} +$staging = Assert-UserPath (Join-Path $userPath ('.quicker/agent-integrations/staging/' + [Guid]::NewGuid().ToString('N'))) +[void][IO.Directory]::CreateDirectory($staging) +Get-ChildItem -LiteralPath $source -Force | Copy-Item -Destination $staging -Recurse -Force +$files = @{} +foreach ($file in Get-ChildItem -LiteralPath $staging -Recurse -File -Force) { + $files[$file.FullName.Substring($staging.Length + 1).Replace('\', '/')] = Get-PackageHash $file.FullName +} +$newMarker = @{ owner = 'QuickerOrg/quicker-agent-integrations'; client = $Client; version = '0.2.0'; files = $files } +if ($server) { $newMarker['server'] = $server } +Write-Json (Join-Path $staging '.quicker-managed.json') $newMarker +$backup = $null +if (Test-Path -LiteralPath $target) { + $backup = Assert-UserPath (Join-Path $userPath ('.quicker/agent-integrations/backups/' + $Client + '-' + [Guid]::NewGuid().ToString('N'))) + [void][IO.Directory]::CreateDirectory((Split-Path -Parent $backup)) + Move-Item -LiteralPath $target -Destination $backup +} +try { + [void][IO.Directory]::CreateDirectory((Split-Path -Parent $target)) + Move-Item -LiteralPath $staging -Destination $target + if ($configPath) { + if (Test-Path -LiteralPath $configPath) { + $configBackup = Assert-UserPath ($configPath + '.quicker-' + [Guid]::NewGuid().ToString('N') + '.bak') + Copy-Item -LiteralPath $configPath -Destination $configBackup + } + $config[$section]['quicker'] = $server + Write-Json $configPath $config + } +} catch { + [void](Assert-UserPath $target) + if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Recurse -Force } + if ($backup) { Move-Item -LiteralPath $backup -Destination $target } + throw +} +Write-Output "Installed Quicker for $Client at $target" +if ($Client -eq 'cursor') { Write-Output 'Cursor IDE: Developer: Reload Window. Cursor CLI: start a new task; use --plugin-dir with the installed directory if local plugins are not discovered.' } +else { Write-Output 'Restart the MCP server or start a new client task.' } +Write-Output 'Enable MCP and allow writes in Quicker Settings > Agent; complete Quicker client consent when prompted.' diff --git a/scripts/sync-packages.py b/scripts/sync-packages.py new file mode 100644 index 0000000..8c3e5e0 --- /dev/null +++ b/scripts/sync-packages.py @@ -0,0 +1,28 @@ +"""Materialize shared sources into self-contained client packages.""" +import argparse +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PACKAGES = ("quicker", "quicker-cursor", "quicker-claude", "quicker-mcp") +FILES = ("scripts/quicker-mcp.ps1", "skills/write-action/SKILL.md", "skills/write-action/references/connection.md") + + +def sync(check=False): + stale = [] + for package in PACKAGES: + for relative in FILES: + source = (ROOT / "shared" / relative).read_bytes() + target = ROOT / "plugins" / package / relative + if not target.exists() or target.read_bytes() != source: + stale.append(str(target.relative_to(ROOT))) + if not check: + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(source) + if check and stale: + raise SystemExit("Run python scripts/sync-packages.py: " + ", ".join(stale)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + sync(parser.parse_args().check) diff --git a/shared/scripts/quicker-mcp.ps1 b/shared/scripts/quicker-mcp.ps1 new file mode 100644 index 0000000..3811fe8 --- /dev/null +++ b/shared/scripts/quicker-mcp.ps1 @@ -0,0 +1,285 @@ +[CmdletBinding()] +param( + [string]$SettingsPath = '', + [ValidateRange(1, 600)] + [int]$RequestTimeoutSeconds = 180, + [ValidateSet('codex', 'cursor', 'claude', 'vscode', 'gemini')] + [string]$Client = 'codex', + [switch]$Check +) + +# A transport adapter only: Quicker owns tools, authoring state and approval policy. +# Keep JSON payloads as text so Windows PowerShell never rewrites schemas or numbers. +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$utf8 = New-Object System.Text.UTF8Encoding($false) +[Console]::InputEncoding = $utf8 +[Console]::OutputEncoding = $utf8 +$OutputEncoding = $utf8 +Add-Type -AssemblyName System.Web.Extensions +$jsonReader = New-Object System.Web.Script.Serialization.JavaScriptSerializer +$jsonReader.MaxJsonLength = [int]::MaxValue +$jsonReader.RecursionLimit = 256 +if ([string]::IsNullOrWhiteSpace($SettingsPath)) { + $SettingsPath = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.quicker\mcp\server.json' +} + +function Write-ProtocolLine([string]$Text) { + [Console]::Out.WriteLine($Text) + [Console]::Out.Flush() +} + +function Compress-JsonText([string]$Text) { + $builder = New-Object System.Text.StringBuilder + $inString = $false + $escaped = $false + foreach ($character in $Text.ToCharArray()) { + if ($inString) { + [void]$builder.Append($character) + if ($escaped) { $escaped = $false } + elseif ($character -eq '\') { $escaped = $true } + elseif ($character -eq '"') { $inString = $false } + } + elseif ($character -eq '"') { + $inString = $true + [void]$builder.Append($character) + } + elseif (-not [char]::IsWhiteSpace($character)) { [void]$builder.Append($character) } + } + return $builder.ToString() +} + +function Get-RpcIdText([string]$Text) { + # Read only the top-level id token, preserving a numeric id's exact spelling. + $depth = 0 + for ($index = 0; $index -lt $Text.Length; $index++) { + $character = $Text[$index] + if ($character -eq '{' -or $character -eq '[') { $depth++; continue } + if ($character -eq '}' -or $character -eq ']') { $depth--; continue } + if ($character -ne '"') { continue } + $start = $index + $index++ + for (; $index -lt $Text.Length; $index++) { + if ($Text[$index] -eq '\') { $index++; continue } + if ($Text[$index] -eq '"') { break } + } + if ($depth -ne 1) { continue } + $after = $index + 1 + while ($after -lt $Text.Length -and [char]::IsWhiteSpace($Text[$after])) { $after++ } + if ($after -ge $Text.Length -or $Text[$after] -ne ':') { continue } + $keyToken = $Text.Substring($start, $index - $start + 1) + $key = $jsonReader.DeserializeObject('{"key":' + $keyToken + '}') + if ($key.key -cne 'id') { continue } + $valueStart = $after + 1 + while ($valueStart -lt $Text.Length -and [char]::IsWhiteSpace($Text[$valueStart])) { $valueStart++ } + $valueEnd = $valueStart + if ($Text[$valueStart] -eq '"') { + $valueEnd++ + for (; $valueEnd -lt $Text.Length; $valueEnd++) { + if ($Text[$valueEnd] -eq '\') { $valueEnd++; continue } + if ($Text[$valueEnd] -eq '"') { $valueEnd++; break } + } + } + else { + while ($valueEnd -lt $Text.Length -and $Text[$valueEnd] -ne ',' -and $Text[$valueEnd] -ne '}') { $valueEnd++ } + } + return $Text.Substring($valueStart, $valueEnd - $valueStart).Trim() + } + return $null +} + +function Write-RpcFailure { + param([string]$IdText, [int]$Code, [string]$FailureCode, [string]$Message, + [bool]$StateUnknown = $false, [int]$HttpStatus = 0) + if ([string]::IsNullOrEmpty($IdText)) { + # Notifications must not receive JSON-RPC replies. Diagnostics never contain input data. + [Console]::Error.WriteLine('Quicker MCP: ' + $FailureCode + '. ' + $Message) + return + } + $data = [ordered]@{ code = $FailureCode; stateUnknown = $StateUnknown } + if ($HttpStatus -ne 0) { $data.httpStatus = $HttpStatus } + $errorBody = [ordered]@{ code = $Code; message = $Message; data = $data } | ConvertTo-Json -Depth 8 -Compress + Write-ProtocolLine ('{"jsonrpc":"2.0","id":' + $IdText + ',"error":' + $errorBody + '}') +} + +function Read-McpSettings { + if (-not [System.IO.File]::Exists($SettingsPath)) { + return @{ Code = 'settings_missing'; Message = 'Enable MCP Server in a Debug build of Quicker first.' } + } + try { + $settings = $jsonReader.DeserializeObject([System.IO.File]::ReadAllText($SettingsPath, $utf8)) + if ($null -eq $settings -or $settings -is [array]) { throw 'invalid' } + $port = 0 + if (-not [int]::TryParse([string]$settings.Port, [ref]$port) -or $port -lt 1024 -or $port -gt 65535) { + throw 'invalid' + } + $tokenPresent = $settings.Token -is [string] -and -not [string]::IsNullOrWhiteSpace($settings.Token) + if ($tokenPresent -and $settings.Token -match '\s') { throw 'invalid' } + $enabled = $settings.Enabled -is [bool] -and $settings.Enabled + $allowWrites = $settings.AllowWrites -is [bool] -and $settings.AllowWrites + return @{ Enabled = $enabled; AllowWrites = $allowWrites; Port = $port; + TokenPresent = $tokenPresent; Token = $settings.Token } + } + catch { + return @{ Code = 'settings_invalid'; Message = 'Quicker MCP settings could not be read. Check them in Quicker.' } + } +} + +if ($Check) { + $settings = Read-McpSettings + if ($settings.Code) { + Write-ProtocolLine (([ordered]@{ ok = $false; code = $settings.Code; message = $settings.Message }) | ConvertTo-Json -Compress) + exit 1 + } + $reachable = $false + $tcpClient = New-Object System.Net.Sockets.TcpClient + try { + $pending = $tcpClient.BeginConnect('127.0.0.1', $settings.Port, $null, $null) + if ($pending.AsyncWaitHandle.WaitOne(2000)) { + $tcpClient.EndConnect($pending) + $reachable = $tcpClient.Connected + } + $pending.AsyncWaitHandle.Close() + } + catch { $reachable = $false } + finally { $tcpClient.Close() } + $ready = $settings.Enabled -and $settings.TokenPresent -and $reachable + Write-ProtocolLine (([ordered]@{ ok = [bool]$ready; enabled = [bool]$settings.Enabled; + allowWrites = [bool]$settings.AllowWrites; port = $settings.Port; + tokenPresent = [bool]$settings.TokenPresent; portReachable = $reachable }) | ConvertTo-Json -Compress) + if ($ready) { exit 0 } + exit 1 +} + +$protocolVersion = $null +while ($null -ne ($line = [Console]::In.ReadLine())) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $idText = $null + try { + $rpc = $jsonReader.DeserializeObject($line) + $idText = Get-RpcIdText $line + if ($null -eq $rpc -or $rpc -is [array] -or $rpc.jsonrpc -cne '2.0' -or + $rpc.method -isnot [string] -or [string]::IsNullOrWhiteSpace($rpc.method)) { + throw 'invalid request' + } + if ($null -ne $rpc.id -and ($rpc.id -is [bool] -or ($rpc.id -isnot [string] -and $rpc.id -isnot [ValueType]))) { throw 'invalid id' } + } + catch { + Write-RpcFailure -IdText 'null' -Code -32600 -FailureCode 'invalid_request' -Message 'Expected one JSON-RPC 2.0 request or notification per line.' + continue + } + + $settings = Read-McpSettings + if ($settings.Code) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode $settings.Code -Message $settings.Message + continue + } + if (-not $settings.Enabled) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'server_disabled' -Message 'Enable MCP Server in Quicker settings.' + continue + } + if (-not $settings.TokenPresent) { + Write-RpcFailure -IdText $idText -Code -32001 -FailureCode 'token_missing' -Message 'Quicker MCP has no token. Check MCP Server settings in Quicker.' + continue + } + + $request = $null + $response = $null + $mayHaveSent = $false + $forwardedResponse = $false + try { + # Only the persisted port is configurable. Never send the token to another host, + # an HTTP proxy, or a redirect target. + $request = [System.Net.HttpWebRequest]::Create('http://127.0.0.1:' + $settings.Port + '/mcp') + $request.Method = 'POST' + $request.Proxy = $null + $request.AllowAutoRedirect = $false + # Avoid automatic replay when a reused keep-alive socket is closed by the server. + $request.KeepAlive = $false + $request.Timeout = $RequestTimeoutSeconds * 1000 + $request.ReadWriteTimeout = $RequestTimeoutSeconds * 1000 + $request.ContentType = 'application/json; charset=utf-8' + $request.Accept = 'application/json, text/event-stream' + $request.Headers['Authorization'] = 'Bearer ' + $settings.Token + $request.Headers['Mcp-Client-Info'] = ($Client + '-quicker-plugin') + if ($protocolVersion) { $request.Headers['MCP-Protocol-Version'] = $protocolVersion } + $request.ServicePoint.Expect100Continue = $false + $bytes = $utf8.GetBytes($line) + $request.ContentLength = $bytes.Length + $mayHaveSent = $true + $stream = $request.GetRequestStream() + try { $stream.Write($bytes, 0, $bytes.Length) } + finally { $stream.Dispose() } + try { $response = $request.GetResponse() } + catch [System.Net.WebException] { + if ($null -eq $_.Exception.Response) { throw } + $response = $_.Exception.Response + } + $status = [int]$response.StatusCode + if ($status -lt 200 -or $status -ge 300) { + $failureCode = 'http_error' + $message = 'Quicker MCP rejected the HTTP request. Check Quicker before retrying.' + $stateUnknown = $status -ge 500 + if ($status -eq 401) { $failureCode = 'unauthorized'; $message = 'Quicker rejected the MCP token. Check MCP settings in Quicker.' } + elseif ($status -eq 403) { $failureCode = 'client_not_approved'; $message = 'Approve the connecting MCP client in Quicker, then reconnect.' } + elseif ($status -ge 300 -and $status -lt 400) { $failureCode = 'redirect_refused'; $message = 'Quicker MCP returned a redirect. Redirects are refused; check the local endpoint.' } + Write-RpcFailure -IdText $idText -Code -32003 -FailureCode $failureCode -Message $message -StateUnknown $stateUnknown -HttpStatus $status + continue + } + if ($status -eq 202) { + if ($idText) { throw 'missing response' } + continue + } + $reader = New-Object System.IO.StreamReader($response.GetResponseStream(), $utf8) + try { $body = $reader.ReadToEnd() } + finally { $reader.Dispose() } + if ([string]::IsNullOrWhiteSpace($body)) { + if ($idText) { throw 'missing response' } + continue + } + $payloads = New-Object 'System.Collections.Generic.List[string]' + if ($response.ContentType -match '^text/event-stream(?:;|$)') { + $eventData = New-Object 'System.Collections.Generic.List[string]' + foreach ($eventLine in ($body -split '\r\n|\n|\r')) { + if ($eventLine.Length -eq 0) { + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")); $eventData.Clear() } + } + elseif ($eventLine.StartsWith('data:')) { + $value = $eventLine.Substring(5) + if ($value.StartsWith(' ')) { $value = $value.Substring(1) } + $eventData.Add($value) + } + } + if ($eventData.Count -gt 0) { $payloads.Add(($eventData -join "`n")) } + } + elseif ($response.ContentType -match '^application/json(?:;|$)') { $payloads.Add($body) } + else { throw 'unsupported response' } + + foreach ($payload in $payloads) { + $parsed = $jsonReader.DeserializeObject($payload) + if ($null -eq $parsed -or $parsed -is [array] -or $parsed.jsonrpc -cne '2.0') { throw 'invalid response' } + $responseId = Get-RpcIdText $payload + if ($null -ne $responseId) { + if (-not $idText -or $forwardedResponse -or $parsed.id -cne $rpc.id) { throw 'unexpected response id' } + if ($rpc.method -ceq 'initialize' -and $parsed.result.protocolVersion -is [string]) { + $version = $parsed.result.protocolVersion + if ($version -match '^\d{4}-\d{2}-\d{2}$') { $protocolVersion = $version } + } + $forwardedResponse = $true + } + elseif ($parsed.method -isnot [string]) { throw 'invalid notification' } + Write-ProtocolLine (Compress-JsonText $payload) + } + if ($idText -and -not $forwardedResponse) { throw 'missing response' } + } + catch { + if (-not $forwardedResponse) { + Write-RpcFailure -IdText $idText -Code -32002 -FailureCode 'transport_failed' -StateUnknown $mayHaveSent -Message 'The Quicker MCP request did not produce a complete response. Its outcome may be unknown; inspect Quicker and reopen or reread the action before retrying a write.' + } + else { [Console]::Error.WriteLine('Quicker MCP: invalid data after the completed response.') } + } + finally { + if ($null -ne $response) { $response.Dispose() } + if ($null -ne $request) { $request.Abort() } + } +} diff --git a/shared/skills/write-action/SKILL.md b/shared/skills/write-action/SKILL.md new file mode 100644 index 0000000..6900424 --- /dev/null +++ b/shared/skills/write-action/SKILL.md @@ -0,0 +1,28 @@ +--- +name: write-action +description: Create, edit, save and preview Windows automation actions in a running Quicker through its MCP tools. Use when the user asks to write or modify a Quicker action, or consult Quicker step knowledge. Does not apply to developing the Quicker application's source code. +--- + +# Write a Quicker action + +Use the Quicker plugin's MCP tools, whose names may have a client namespace prefix. These edit Quicker's virtual workspace, not the agent repository. In particular, use Quicker's read_file, write_file, edit_file and grep for slots and @knowledge; ordinary local filesystem tools cannot resolve them. + +The live server's initialize instructions and returned capability rules describe the current authoring protocol. Read all content blocks, including attachedRules. Tool availability and current knowledge are authoritative; this plugin does not carry a duplicate module catalog. + +## Authoring workflow + +1. Load the runtime grammar using skill_load with `{ "id": "action-source" }` before editing. For an unfamiliar step, follow its steps skill: grep one keyword under @knowledge/catalog, then read the matching module YAML. Use quicker_eval with steps.resolve only for the needed branch or referenced docs. Do not invent step names or input/output keys. Host JS uses JavaScript; action `$=` expressions use Quicker's C# expression syntax. +2. For a new action use quicker_create with a concise title. To edit an existing one, use quicker_search once, identify the intended result, then quicker_open with its actionId. Opening a formal action creates an editing draft; it does not overwrite the original. +3. Keep the returned slot and actionId. Always address files explicitly as `/program.source.yaml`, `/info.yaml` or `/files/...`, and save with quicker_save `{ "path": "" }`. Several tasks can share an agent backend process and Quicker's current location; do not rely on the implicit working directory or concurrently edit the same slot. +4. Write the program in program.source.yaml; use info.yaml for title, description and appearance. data.json and info.json are Host internals. quicker_save validates and stores the draft in Quicker's 暂存区. Inspect its result and correct reported errors before reporting completion. +5. Use quicker_preview `{ "target": "" }` when showing the result helps the user. It opens Quicker's designer. Only open a browser URL actually returned by the tool; do not construct one from a designer route. + +A request to write an action normally ends with a saved draft. Promote a new draft only when the user has specified an unambiguous existing destination scene; otherwise explain the 暂存区 → 保留到… flow. Overwrite a formal action or run an action only within the user's requested scope, using quicker_overwrite or quicker_run and Quicker's approval flow. State whether the action was only saved, also previewed, or actually run. + +## Failure handling + +Check JSON-RPC errors, MCP isError, `{ok:false,code,message}`, and quicker_eval's `{error:...}` envelope. Do not infer success from receiving text. + +When a mutation times out, the connection drops, or stateUnknown is returned, inspect the relevant slot/action before retrying. If create reports programCreated with an actionId, reopen that action rather than creating another. For catalog_stale or catalog_freshness_unknown, inspect the current action and preserve pending changes; do not blindly overwrite or re-open over dirty work. + +If tools are unavailable, consult [connection.md](references/connection.md). A missing write tool usually means Quicker's 允许 MCP 写入 switch is off. The plugin must not edit server.json or clients.json to enable access, grant consent, or change approval settings. diff --git a/shared/skills/write-action/references/connection.md b/shared/skills/write-action/references/connection.md new file mode 100644 index 0000000..04ed0f4 --- /dev/null +++ b/shared/skills/write-action/references/connection.md @@ -0,0 +1,18 @@ +# Connection troubleshooting + +Requires Windows PowerShell 5.1 and a Quicker build with Settings → Agent → Enable MCP. Both supported Debug and Release builds work; older releases without that setting do not. + +Run powershell.exe -NoProfile -ExecutionPolicy Bypass -File "/scripts/quicker-mcp.ps1" -Check for a redacted configuration/TCP check. This does not prove client consent or write access. + +The relay reads the current port and token from the local Quicker configuration for each request. Keep the token out of commands, client configuration, logs and conversation. It uses only loopback and does not follow proxies or redirects. + +- Missing/disabled MCP: open Quicker Settings → Agent and enable MCP. +- Connection refused: start the supported Quicker build and check the configured port. +- HTTP 401: check the running instance; token rotation is picked up on the next read. +- HTTP 403 or a first connection waiting: complete Quicker's consent UI for the actual connecting client. The relay never grants itself access. +- Write tools absent: enable 允许 MCP 写入 in Quicker, keep the existing approval mode, then reconnect or start a new agent task. +- A write timeout can leave an unknown outcome: inspect the slot before retrying. + +After installation/update, reload the client and create a new task. Cursor IDE: Developer: Reload Window; Cursor CLI: restart agent. Claude Code: /reload-plugins or restart. VS Code: MCP: List Servers → Quicker → Restart. Gemini CLI: restart. + +Plugins ship their own relay. Cursor and Claude expand their respective plugin-root variables; Codex uses package-relative cwd. Generic MCP configuration points at the installed copy, never the development checkout. Do not edit server.json or clients.json to grant access. diff --git a/tests/test_install_clients.py b/tests/test_install_clients.py new file mode 100644 index 0000000..85f6844 --- /dev/null +++ b/tests/test_install_clients.py @@ -0,0 +1,118 @@ +"""Exercise installers against isolated profiles, never the signed-in user's settings.""" +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +from test_quicker_mcp import ROOT, TEMP_ROOT, POWERSHELL, MockHost, encoded, request, reply + + +class PackageTests(unittest.TestCase): + def test_shared_sources_and_manifests(self): + subprocess.run(['python', str(ROOT / 'scripts/sync-packages.py'), '--check'], check=True) + for client in ('cursor', 'claude'): + package = ROOT / 'plugins' / ('quicker-' + client) + manifest = json.loads((package / ('.' + client + '-plugin') / 'plugin.json').read_text(encoding='utf-8')) + self.assertEqual(manifest['name'], 'quicker') + self.assertTrue((package / 'skills/write-action/SKILL.md').is_file()) + market = json.loads((ROOT / ('.' + client + '-plugin') / 'marketplace.json').read_text(encoding='utf-8')) + self.assertEqual((ROOT / market['plugins'][0]['source']).resolve(), package.resolve()) + + +@unittest.skipUnless(POWERSHELL, 'Windows PowerShell required') +class InstallTests(unittest.TestCase): + def setUp(self): + TEMP_ROOT.mkdir(exist_ok=True) + self.temp = tempfile.TemporaryDirectory(prefix='安装 用户 ', dir=TEMP_ROOT) + self.addCleanup(self.temp.cleanup) + self.profile = Path(self.temp.name) + + def install(self, client, *args, success=True): + result = subprocess.run([str(POWERSHELL), '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', + str(ROOT / 'scripts/install-client.ps1'), '-Client', client, + '-UserRoot', str(self.profile), *args], capture_output=True, timeout=45) + if success: + self.assertEqual(result.returncode, 0, result.stderr.decode(errors='replace')) + else: + self.assertNotEqual(result.returncode, 0) + return result + + def config(self, client): + return self.profile / ('AppData/Roaming/Code/User/mcp.json' if client == 'vscode' else '.gemini/settings.json') + + def test_install_update_uninstall_preserves_other_configuration(self): + for client in ('vscode', 'gemini'): + with self.subTest(client=client): + path = self.config(client) + section = 'servers' if client == 'vscode' else 'mcpServers' + original = {section: {'other': {'command': 'keep-me'}}, 'otherPreference': {'unicode': '保留', 'enabled': True}} + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(encoded(original)) + self.install(client) + config = json.loads(path.read_text(encoding='utf-8')) + entry = config[section]['quicker'] + self.assertEqual(config[section]['other'], original[section]['other']) + self.assertEqual(config['otherPreference'], original['otherPreference']) + self.assertNotIn('token', json.dumps(entry).lower()) + self.assertTrue(Path(entry['args'][6]).is_file()) + self.install(client) + self.install(client, '-Uninstall') + self.assertEqual(json.loads(path.read_text(encoding='utf-8')), original) + + def test_foreign_mcp_entry_is_not_replaced(self): + path = self.config('gemini') + path.parent.mkdir(parents=True) + contents = b'{"mcpServers":{"quicker":{"command":"foreign"}}}' + path.write_bytes(contents) + self.install('gemini', success=False) + self.assertEqual(path.read_bytes(), contents) + self.assertFalse((self.profile / '.quicker/agent-integrations/gemini').exists()) + + def test_cursor_package_discovery_update_and_local_edits(self): + self.install('cursor') + target = self.profile / '.cursor/plugins/local/quicker' + self.assertTrue((target / '.cursor-plugin/plugin.json').is_file()) + self.assertTrue((target / 'mcp.json').is_file()) + self.install('cursor') + (target / 'custom.txt').write_text('keep', encoding='utf-8') + self.install('cursor', success=False) + self.install('cursor', '-Uninstall', success=False) + self.assertEqual((target / 'custom.txt').read_text(), 'keep') + + def test_invalid_json_leaves_existing_file_untouched(self): + path = self.config('vscode') + path.parent.mkdir(parents=True) + contents = b'{ // user comment\n "servers": {} }' + path.write_bytes(contents) + self.install('vscode', success=False) + self.assertEqual(path.read_bytes(), contents) + + def test_foreign_local_plugin_is_not_replaced(self): + target = self.profile / '.cursor/plugins/local/quicker' + target.mkdir(parents=True) + (target / 'keep').write_text('foreign') + self.install('cursor', success=False) + self.assertEqual((target / 'keep').read_text(), 'foreign') + + def test_each_plugin_launches_from_unrelated_directory(self): + for client, variable in [('cursor', 'CURSOR_PLUGIN_ROOT'), ('claude', 'CLAUDE_PLUGIN_ROOT')]: + with self.subTest(client=client): + host = MockHost() + try: + settings = self.profile / 'fixture.json' + settings.write_bytes(encoded({'Enabled': True, 'Port': host.port, 'Token': 'test-only-token'})) + package = ROOT / 'plugins' / ('quicker-' + client) + config = json.loads((package / ('mcp.json' if client == 'cursor' else '.mcp.json')).read_text(encoding='utf-8'))['mcpServers']['quicker'] + args = [a.replace('${' + variable + '}', str(package)) for a in config['args']] + host.respond(reply(1, {'tools': [], 'extra': '中文保真'})) + result = subprocess.run([str(POWERSHELL), *args, '-SettingsPath', str(settings)], + input=encoded(request()) + b'\n', capture_output=True, cwd=self.profile, timeout=35) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)['result']['extra'], '中文保真') + headers = {k.lower(): v for k, v in host.requests[0]['headers'].items()} + self.assertEqual(headers['mcp-client-info'], client + '-quicker-plugin') + self.assertNotIn(b'test-only-token', result.stdout + result.stderr) + finally: + host.close() diff --git a/tests/test_quicker_mcp.py b/tests/test_quicker_mcp.py index 96dd2fd..72d57e0 100644 --- a/tests/test_quicker_mcp.py +++ b/tests/test_quicker_mcp.py @@ -145,7 +145,7 @@ def close(self): if self.process.stdin and not self.process.stdin.closed: self.process.stdin.close() try: - self.process.wait(timeout=5) + self.process.wait(timeout=PROCESS_TIMEOUT_SECONDS) except subprocess.TimeoutExpired: self.process.kill() self.process.wait(timeout=2)