diffing 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -100
- package/dist/apply-suggestion-BAuyCYk6.mjs +54 -0
- package/dist/cli-agent-BkkRbKqq.mjs +513 -0
- package/dist/cli-agent-BlMtx1Tx.mjs +736 -0
- package/dist/cli-agent-C4Fb90GL.mjs +728 -0
- package/dist/cli-agent-ChALuNbw.mjs +841 -0
- package/dist/cli-agent-D71pq2ep.mjs +841 -0
- package/dist/cli-agent-qBT8clNM.mjs +841 -0
- package/dist/cli-gh-6qiblgj7.mjs +150 -0
- package/dist/cli-gh-D5OCnjNe.mjs +150 -0
- package/dist/cli-gh-DgqscIkg.mjs +147 -0
- package/dist/cli-gh-KjPnyST6.mjs +150 -0
- package/dist/cli.mjs +38 -66
- package/dist/client/assets/{angular-html-BotC7B4-.js → angular-html-Dh86jUvr.js} +1 -1
- package/dist/client/assets/{angular-ts-DQTHeTJm.js → angular-ts-Bh4wRJtT.js} +1 -1
- package/dist/client/assets/{apl-Bwz5Dpd_.js → apl-BSXBHduT.js} +1 -1
- package/dist/client/assets/{arc-D-TSgv-7.js → arc-CRQlhBrV.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-DLe25t8t.js → architectureDiagram-3BPJPVTR-BXq3UvEv.js} +1 -1
- package/dist/client/assets/{astro-DjkoD2fR.js → astro-Bh00klUw.js} +1 -1
- package/dist/client/assets/{blade-Cap2uJIA.js → blade-CisA3zhm.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-B-bihLYO.js → blockDiagram-GPEHLZMM-Bw-PTAJr.js} +1 -1
- package/dist/client/assets/{c-C7JoAATL.js → c-D-pHVWrf.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CoCcjXEC.js → c4Diagram-AAUBKEIU-C0XyiqC_.js} +1 -1
- package/dist/client/assets/channel-CDxgOu-3.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Sm_uQrGq.js → chunk-2J33WTMH-DekjZXNj.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-Ct83Sp4P.js → chunk-3OPIFGDE-BtROpcL8.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DrqChk1a.js → chunk-4BX2VUAB-C92XLTlx.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-CodeFb9E.js → chunk-55IACEB6-CceUgh8N.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-CpEx4dmS.js → chunk-5ZQYHXKU-DSbYPAe7.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-D3U6TUmk.js → chunk-727SXJPM-DVzUJa-9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Dr-_DyBy.js → chunk-AQP2D5EJ-6n3Z_IFa.js} +1 -1
- package/dist/client/assets/{chunk-BSJP7CBP-Bnyml8mU.js → chunk-BSJP7CBP-BgVLnkxr.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-BJSFGTlk.js → chunk-CSCIHK7Q-CkGMXntp.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-BqiwDIwx.js → chunk-FMBD7UC4-BM8lrLBo.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-D-H-HBRn.js → chunk-KSCS5N6A-Bm2pjMQg.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-CVMSAF7h.js → chunk-L5ZTLDWV-VL17ikXM.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-BQsF6HF3.js → chunk-LZXEDZCA-B629g67M.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-eoM20ySO.js → chunk-ND2GUHAM-CjZGwhKA.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-B1iU0i4a.js → chunk-NZK2D7GU-DGsLjESY.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-CcIPuHUC.js → chunk-O5CBEL6O-EyUrGY3l.js} +1 -1
- package/dist/client/assets/chunk-QZHKN3VN-DJCeMpP8.js +1 -0
- package/dist/client/assets/chunk-WU5MYG2G-BbuOkMaH.js +1 -0
- package/dist/client/assets/{chunk-XPW4576I-jSj3E63-.js → chunk-XPW4576I-C2TeAQmQ.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-D5oJbiED.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-5MJt5ZxQ.js +1 -0
- package/dist/client/assets/{cobol-DkiK_msx.js → cobol-DsRZMP16.js} +1 -1
- package/dist/client/assets/{coffee-f7z4wIqP.js → coffee-DP0-MMsE.js} +1 -1
- package/dist/client/assets/{cose-bilkent-S5V4N54A-Dikk-RXu.js → cose-bilkent-S5V4N54A-DMmK0b0W.js} +1 -1
- package/dist/client/assets/{cpp-DB_FV8ps.js → cpp-B6XYbbze.js} +1 -1
- package/dist/client/assets/{crystal-BRBnyZFz.js → crystal-dBLPFLMp.js} +1 -1
- package/dist/client/assets/{css-GpNBLqRF.js → css-tgwuSZL0.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-tErJNBcm.js → dagre-BM42HDAG-CgNkf0TR.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-D0-DgitC.js → diagram-2AECGRRQ-ocAqn0LV.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-DqMjQTtW.js → diagram-5GNKFQAL-CB1Yo4Gc.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-CP7V9tqS.js → diagram-KO2AKTUF-B2vpMBRT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-tCGdsz4p.js → diagram-LMA3HP47-DuxZOaQS.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-DtDnaqso.js → diagram-OG6HWLK6-CkShDmcv.js} +1 -1
- package/dist/client/assets/{dist-B5Eb-OKZ.js → dist-BgZIvk5w.js} +1 -1
- package/dist/client/assets/{edge-DqnWvViE.js → edge-Bokubn8E.js} +1 -1
- package/dist/client/assets/{elixir-BSuxOIIa.js → elixir-BEzcf3Xg.js} +1 -1
- package/dist/client/assets/{elm-B6U1Is0L.js → elm-vJyLTAER.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-C5S0U2x3.js → erDiagram-TEJ5UH35-BFV1NQAH.js} +1 -1
- package/dist/client/assets/{erb-kK3QM18H.js → erb-DDkAmjBS.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-NNqmBYk2.js → flowDiagram-I6XJVG4X-DU8xbFdO.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-8ng09X8S.js → ganttDiagram-6RSMTGT7-DCb7Qh0c.js} +1 -1
- package/dist/client/assets/{git-rebase-BDKTWtxr.js → git-rebase-DWcifAdK.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-D_HBvwVl.js → gitGraphDiagram-PVQCEYII-C_Tw-piE.js} +1 -1
- package/dist/client/assets/{glimmer-js-BthGlRra.js → glimmer-js--HDru5ys.js} +1 -1
- package/dist/client/assets/{glimmer-ts-CS8Z9z2h.js → glimmer-ts-DAdXisHh.js} +1 -1
- package/dist/client/assets/{glsl-C5ERhU2v.js → glsl-CmIRlJht.js} +1 -1
- package/dist/client/assets/{graphql-sfgTqbIp.js → graphql-tx4-7I3U.js} +1 -1
- package/dist/client/assets/{hack-EeOmOf4N.js → hack-yA0XjhF7.js} +1 -1
- package/dist/client/assets/{haml-DVf5-Ndq.js → haml-BR5MUE1b.js} +1 -1
- package/dist/client/assets/{handlebars-BSLgW49o.js → handlebars-BPOF7iOf.js} +1 -1
- package/dist/client/assets/{html-DoOSf-hz.js → html-BbteRk96.js} +1 -1
- package/dist/client/assets/{html-derivative-BM0DLd1Z.js → html-derivative-59NNrcMm.js} +1 -1
- package/dist/client/assets/{http-d2p18QI6.js → http-D3odGQI4.js} +1 -1
- package/dist/client/assets/{hurl-CH2I2f66.js → hurl-BLTj8fPj.js} +1 -1
- package/dist/client/assets/index-BZcKS8Sb.css +1 -0
- package/dist/client/assets/{index-CJI1E0bV.js → index-DHXpO7eS.js} +137 -104
- package/dist/client/assets/{infoDiagram-5YYISTIA-Cgg2FsCN.js → infoDiagram-5YYISTIA-CI6hJyRl.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-pYRQFecr.js → ishikawaDiagram-YF4QCWOH-gMS1P1LQ.js} +1 -1
- package/dist/client/assets/{java-BFiqhlWm.js → java-CsViuzs_.js} +1 -1
- package/dist/client/assets/{javascript-YWa7zRPZ.js → javascript-DQgsl2Dh.js} +1 -1
- package/dist/client/assets/{jinja-h_UdmDAn.js → jinja-DQNp2NUA.js} +1 -1
- package/dist/client/assets/{jison-cOvcjPYz.js → jison-CDDOZQdc.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-D3EBlW7m.js → journeyDiagram-JHISSGLW-BTQ_Ws_h.js} +1 -1
- package/dist/client/assets/{json-DT0GjoRi.js → json-DP4TqCuE.js} +1 -1
- package/dist/client/assets/{jsx-BaYjgZVH.js → jsx-Df5hn0Tf.js} +1 -1
- package/dist/client/assets/{julia-Cg36ySxC.js → julia--u33yGLi.js} +1 -1
- package/dist/client/assets/{just-KYC4U1y4.js → just-BCpMmSOD.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-DA42mt4M.js → kanban-definition-UN3LZRKU-CvqGbLR_.js} +1 -1
- package/dist/client/assets/{latex-DpuXxlsH.js → latex-D8D4jZGw.js} +1 -1
- package/dist/client/assets/{line-D91vf_Yl.js → line-DBgaS_RW.js} +1 -1
- package/dist/client/assets/{linear-CL7WzrL5.js → linear-RQVzKNVU.js} +1 -1
- package/dist/client/assets/{liquid-Dua3sjBU.js → liquid-CkH85f12.js} +1 -1
- package/dist/client/assets/{lua-CpDMEbOh.js → lua-iKoV7WE3.js} +1 -1
- package/dist/client/assets/{marko-rM0rxrJ5.js → marko-N8ScQSqT.js} +1 -1
- package/dist/client/assets/{mdc-D-E4W6ne.js → mdc-BvHTOxsp.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BLQDdrsf.js → mermaid-parser.core-CwDRwMR7.js} +1 -1
- package/dist/client/assets/{mermaid.core-BBR2Otc_.js → mermaid.core-D2LDJgXO.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DMrApv-_.js → mindmap-definition-RKZ34NQL-FRnPh642.js} +1 -1
- package/dist/client/assets/{nginx-wZWgcQLM.js → nginx-DGgvASuI.js} +1 -1
- package/dist/client/assets/{nim-Bh7OZsZh.js → nim-D-v9hzvR.js} +1 -1
- package/dist/client/assets/{perl-DtEqNC_V.js → perl-CG8leJ8K.js} +1 -1
- package/dist/client/assets/{php-DX7bHce8.js → php-DvkeSP10.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-B2f9_PO6.js → pieDiagram-4H26LBE5-CepOg38Y.js} +1 -1
- package/dist/client/assets/{pug-r67inScB.js → pug-B4T7-xaL.js} +1 -1
- package/dist/client/assets/{qml-f1bOqUwX.js → qml-BE6D5Bxn.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DgdKno5G.js → quadrantDiagram-W4KKPZXB-DOFAsP60.js} +1 -1
- package/dist/client/assets/{r-Cpm-_vkr.js → r-BKZg9Mcq.js} +1 -1
- package/dist/client/assets/{razor-CEymfLfo.js → razor-D-02oaxL.js} +1 -1
- package/dist/client/assets/{regexp-DCnAJwuE.js → regexp-BDFvAR1k.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-BIRl56-J.js → requirementDiagram-4Y6WPE33-BBVNH-2W.js} +1 -1
- package/dist/client/assets/review-export-D5iIl46_.js +7 -0
- package/dist/client/assets/{rst-Cm1M2uc8.js → rst-BcT26OSj.js} +1 -1
- package/dist/client/assets/{ruby-B49kkGT0.js → ruby-Cq_YQvFT.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-7XZZmV9N.js → sankeyDiagram-5OEKKPKP-BQfCTbBX.js} +1 -1
- package/dist/client/assets/{sas-BOXC9Iq4.js → sas-DN9oTxKY.js} +1 -1
- package/dist/client/assets/{scss-DU2HGWZP.js → scss-BK_sp3z2.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CtIjo3i9.js → sequenceDiagram-3UESZ5HK-CKAW2eyx.js} +1 -1
- package/dist/client/assets/{shellscript-YFjx1NBH.js → shellscript-CvNpOu0J.js} +1 -1
- package/dist/client/assets/{shellsession-4zfyiKm-.js → shellsession-8-yj2cJj.js} +1 -1
- package/dist/client/assets/{soy-CRExh-ql.js → soy-C6Q9atPF.js} +1 -1
- package/dist/client/assets/{sql-2Iw6Ta1F.js → sql-CBL3BuHb.js} +1 -1
- package/dist/client/assets/{src-Dn3_YOHM.js → src-D-VUWIK5.js} +1 -1
- package/dist/client/assets/{stata-DflAcpzB.js → stata-B2Q7Vqzu.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-D_w9F0BN.js → stateDiagram-AJRCARHV-BGJiv0qO.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdUgMghp.js +1 -0
- package/dist/client/assets/{surrealql-CMprrxj1.js → surrealql-C-A2N8vk.js} +1 -1
- package/dist/client/assets/{svelte-Bb_Bxt4a.js → svelte-VAut_-uy.js} +1 -1
- package/dist/client/assets/{templ-MJ_5gYAf.js → templ-mUetM--K.js} +1 -1
- package/dist/client/assets/{tex-BQ33E56K.js → tex-BT53mWGp.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-BzgsTjf8.js → timeline-definition-PNZ67QCA-lVwMt1rk.js} +1 -1
- package/dist/client/assets/{ts-tags-Cddttr6f.js → ts-tags-VkIOkgLN.js} +1 -1
- package/dist/client/assets/{tsx-BFchi_eS.js → tsx-D_aaGHTf.js} +1 -1
- package/dist/client/assets/{twig-CA8ssIYI.js → twig-CufwqKkk.js} +1 -1
- package/dist/client/assets/{typescript-tdiTR9sY.js → typescript-osFD2CEV.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CMjPo4Qf.js → vennDiagram-CIIHVFJN-_K9NtpA5.js} +1 -1
- package/dist/client/assets/{vue-BbZNP0Rd.js → vue-Bub1rXmL.js} +1 -1
- package/dist/client/assets/{vue-html-02g5ipYx.js → vue-html-CN3x2pYg.js} +1 -1
- package/dist/client/assets/{vue-vine-9lCbaeLR.js → vue-vine-C0csYkGm.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cy6qwCgU.js → wardleyDiagram-YWT4CUSO-B2jTMfra.js} +1 -1
- package/dist/client/assets/{xml-BPPCB2P8.js → xml-my6OjeGb.js} +1 -1
- package/dist/client/assets/{xsl-1-u1Beet.js → xsl-Bx0YVn26.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-q9ysdRiS.js → xychartDiagram-2RQKCTM6-DfZ4YYXU.js} +1 -1
- package/dist/client/assets/{yaml-79W-bav7.js → yaml-DanwOypk.js} +1 -1
- package/dist/client/favicon.svg +40 -0
- package/dist/client/index.html +7 -3
- package/dist/comment-format-B5AcGcR-.mjs +118 -0
- package/dist/comment-format-CKTt0fzi.mjs +113 -0
- package/dist/comment-format-DG_PAehC.mjs +117 -0
- package/dist/completions-ZSKYvW06.mjs +126 -0
- package/dist/diff-fingerprint-CWa-soYD.mjs +78 -0
- package/dist/doctor-q6oA64hm.mjs +195 -0
- package/dist/find-tui-binary-CRpbYXLC.mjs +52 -0
- package/dist/git-Czo96ymM.mjs +789 -0
- package/dist/git-DaP_1rVy.mjs +2 -0
- package/dist/github-C9GLNak7.mjs +2 -0
- package/dist/github-HfIlIS-J.mjs +525 -0
- package/dist/mcp-7eXATtez.mjs +1334 -0
- package/dist/mcp-BBk8AIO4.mjs +1334 -0
- package/dist/mcp-BJxrW9dS.mjs +1334 -0
- package/dist/mcp-BgZO-iXI.mjs +1047 -0
- package/dist/mcp-BhT5GgYF.mjs +1334 -0
- package/dist/mcp-BsGdNA88.mjs +1334 -0
- package/dist/mcp-BxBm3d5l.mjs +1047 -0
- package/dist/mcp-Ca9QAlJ0.mjs +1334 -0
- package/dist/mcp-CphIUJUg.mjs +1047 -0
- package/dist/mcp-eor39qmD.mjs +1352 -0
- package/dist/plan-format-BQyx--mp.mjs +144 -0
- package/dist/plan-format-Bi46QS0m.mjs +132 -0
- package/dist/review-export-BoV0884M.mjs +66 -0
- package/dist/review-export-CKTI5Od_.mjs +66 -0
- package/dist/review-export-DjxKBeIZ.mjs +66 -0
- package/dist/server-AisDKkpW.mjs +3227 -0
- package/dist/server-BUsRhL1B.mjs +3283 -0
- package/dist/server-BZBFquG8.mjs +3875 -0
- package/dist/server-Bc2m9R5-.mjs +3263 -0
- package/dist/server-Bmt57wV5.mjs +3875 -0
- package/dist/server-C105rwQw.mjs +3283 -0
- package/dist/server-D0Ni8PKP.mjs +3252 -0
- package/dist/server-D2xEbakj.mjs +3874 -0
- package/dist/server-TOElOuNP.mjs +3258 -0
- package/dist/server-lock-DaSx7DjW.mjs +1049 -0
- package/dist/server-lock-uqSdDYeR.mjs +153 -0
- package/dist/settings-BVnzGFL7.mjs +478 -0
- package/dist/update-check-fe0tR8tx.mjs +129 -0
- package/package.json +1 -1
- package/dist/client/assets/channel-h7e2aSOW.js +0 -1
- package/dist/client/assets/chunk-QZHKN3VN-HzjMGBiA.js +0 -1
- package/dist/client/assets/chunk-WU5MYG2G-D_yZ0nqc.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-eToeXGJV.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-D_EfzX89.js +0 -1
- package/dist/client/assets/index-Ct9zZxJ5.css +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Brvg0TWH.js +0 -1
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
import { a as buildGitDiffArgs, s as parseDiffOptions, t as startServer } from "./server-Bmt57wV5.mjs";
|
|
2
|
+
import { a as removeServerLock, c as formatComments, i as readServerLock, n as diffScopeKey, o as removeServerLockIfOwned, r as isLockAlive, s as writeServerLock, t as acquireServerStartupLease } from "./server-lock-DaSx7DjW.mjs";
|
|
3
|
+
import { n as formatPlanReview } from "./plan-format-B6nbAw_c.mjs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
8
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
//#region src/mcp.ts
|
|
13
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
function readPackageVersion() {
|
|
15
|
+
const packagePath = resolve(moduleDirectory, "..", "package.json");
|
|
16
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
17
|
+
if (typeof pkg.version !== "string" || !pkg.version) throw new Error(`Invalid package version in ${packagePath}`);
|
|
18
|
+
return pkg.version;
|
|
19
|
+
}
|
|
20
|
+
const MCP_VERSION = readPackageVersion();
|
|
21
|
+
const READ_ONLY = {
|
|
22
|
+
readOnlyHint: true,
|
|
23
|
+
destructiveHint: false,
|
|
24
|
+
idempotentHint: true,
|
|
25
|
+
openWorldHint: false
|
|
26
|
+
};
|
|
27
|
+
const MUTATING = {
|
|
28
|
+
readOnlyHint: false,
|
|
29
|
+
destructiveHint: false,
|
|
30
|
+
idempotentHint: false,
|
|
31
|
+
openWorldHint: false
|
|
32
|
+
};
|
|
33
|
+
const IDEMPOTENT_MUTATION = {
|
|
34
|
+
...MUTATING,
|
|
35
|
+
idempotentHint: true
|
|
36
|
+
};
|
|
37
|
+
const AWAIT = {
|
|
38
|
+
readOnlyHint: true,
|
|
39
|
+
destructiveHint: false,
|
|
40
|
+
idempotentHint: false,
|
|
41
|
+
openWorldHint: false
|
|
42
|
+
};
|
|
43
|
+
const commentSchema = z.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
filePath: z.string(),
|
|
46
|
+
side: z.enum(["deletions", "additions"]),
|
|
47
|
+
lineNumber: z.number(),
|
|
48
|
+
startLineNumber: z.number().optional(),
|
|
49
|
+
lineContent: z.string(),
|
|
50
|
+
body: z.string(),
|
|
51
|
+
status: z.enum(["open", "resolved"]),
|
|
52
|
+
createdAt: z.number(),
|
|
53
|
+
replies: z.array(z.object({
|
|
54
|
+
id: z.string(),
|
|
55
|
+
body: z.string(),
|
|
56
|
+
createdAt: z.number(),
|
|
57
|
+
role: z.enum(["user", "agent"]).optional(),
|
|
58
|
+
model: z.string().optional(),
|
|
59
|
+
createdAtPlanVersion: z.number().optional()
|
|
60
|
+
}))
|
|
61
|
+
});
|
|
62
|
+
function textResult(text, structuredContent) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
type: "text",
|
|
66
|
+
text
|
|
67
|
+
}],
|
|
68
|
+
structuredContent
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Resolve and validate an immutable repository binding without invoking a shell. */
|
|
72
|
+
function resolveMcpRepository(repoPath = process.cwd(), explicit = false) {
|
|
73
|
+
if (explicit && !isAbsolute(repoPath)) throw new Error("diffing mcp: --repo must be an absolute path");
|
|
74
|
+
let candidate;
|
|
75
|
+
try {
|
|
76
|
+
candidate = realpathSync(repoPath);
|
|
77
|
+
if (!statSync(candidate).isDirectory()) throw new Error("not a directory");
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(`diffing mcp: repository path is not an accessible directory: ${repoPath}`);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return execFileSync("git", [
|
|
83
|
+
"-C",
|
|
84
|
+
candidate,
|
|
85
|
+
"rev-parse",
|
|
86
|
+
"--show-toplevel"
|
|
87
|
+
], {
|
|
88
|
+
encoding: "utf-8",
|
|
89
|
+
stdio: [
|
|
90
|
+
"ignore",
|
|
91
|
+
"pipe",
|
|
92
|
+
"pipe"
|
|
93
|
+
]
|
|
94
|
+
}).trim();
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`diffing mcp: path is not inside a Git repository: ${candidate}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isLoopbackHost(host) {
|
|
100
|
+
const normalized = host.toLowerCase();
|
|
101
|
+
if (normalized === "localhost" || normalized === "::1" || normalized === "[::1]") return true;
|
|
102
|
+
const octets = normalized.split(".");
|
|
103
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((part) => {
|
|
104
|
+
if (!/^\d{1,3}$/.test(part)) return false;
|
|
105
|
+
const value = Number(part);
|
|
106
|
+
return value >= 0 && value <= 255;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function lockUrl(lock) {
|
|
110
|
+
if ((lock.mode ?? "web") === "tui" || lock.port <= 0) return null;
|
|
111
|
+
if (!isLoopbackHost(lock.host)) return null;
|
|
112
|
+
return `http://${lock.host === "::1" ? "[::1]" : lock.host}:${lock.port}`;
|
|
113
|
+
}
|
|
114
|
+
async function requestJson(base, path, init) {
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(`${base}${path}`, init);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof Error && error.name === "AbortError") throw new Error(`Request to ${path} was cancelled. Retry the MCP tool when ready.`);
|
|
120
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
121
|
+
throw new Error(`Cannot reach the diffing review server at ${base}: ${detail}. Call review_session_status, then start_review_session if needed.`);
|
|
122
|
+
}
|
|
123
|
+
const raw = await response.text();
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
let detail = raw.trim();
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(raw);
|
|
128
|
+
if (typeof parsed.error === "string") detail = parsed.error;
|
|
129
|
+
} catch {}
|
|
130
|
+
throw new Error(`diffing server rejected ${init?.method ?? "GET"} ${path} with HTTP ${response.status}` + (detail ? `: ${detail}` : "") + ".");
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`diffing server returned malformed JSON for ${path}. Check that client and server versions match.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function sameScope(lock, requestedScope, requestedArgs) {
|
|
139
|
+
if (lock.scope) return lock.scope === requestedScope;
|
|
140
|
+
return requestedArgs.length === 0;
|
|
141
|
+
}
|
|
142
|
+
const SAFE_MCP_BOOLEAN_DIFF_ARGS = new Set([
|
|
143
|
+
"--staged",
|
|
144
|
+
"--cached",
|
|
145
|
+
"--merge",
|
|
146
|
+
"--no-indent-heuristic",
|
|
147
|
+
"--ignore-space-change",
|
|
148
|
+
"--ignore-all-space",
|
|
149
|
+
"--ignore-blank-lines",
|
|
150
|
+
"--ignore-cr-at-eol",
|
|
151
|
+
"--function-context",
|
|
152
|
+
"--find-copies-harder",
|
|
153
|
+
"--pickaxe-all",
|
|
154
|
+
"--no-ext-diff",
|
|
155
|
+
"-b",
|
|
156
|
+
"-w",
|
|
157
|
+
"-W"
|
|
158
|
+
]);
|
|
159
|
+
const SAFE_MCP_VALUE_DIFF_ARGS = new Set([
|
|
160
|
+
"--diff-algorithm",
|
|
161
|
+
"--anchored",
|
|
162
|
+
"--ws-error-highlight",
|
|
163
|
+
"--unified",
|
|
164
|
+
"--inter-hunk-context",
|
|
165
|
+
"--find-copies",
|
|
166
|
+
"--find-renames",
|
|
167
|
+
"--break-rewrites",
|
|
168
|
+
"--diff-filter",
|
|
169
|
+
"--ignore-submodules",
|
|
170
|
+
"-U",
|
|
171
|
+
"-C",
|
|
172
|
+
"-M",
|
|
173
|
+
"-B",
|
|
174
|
+
"-S",
|
|
175
|
+
"-G"
|
|
176
|
+
]);
|
|
177
|
+
const SAFE_MCP_ENUM_VALUES = {
|
|
178
|
+
"--diff-algorithm": new Set([
|
|
179
|
+
"minimal",
|
|
180
|
+
"patience",
|
|
181
|
+
"histogram",
|
|
182
|
+
"myers"
|
|
183
|
+
]),
|
|
184
|
+
"--ws-error-highlight": new Set([
|
|
185
|
+
"none",
|
|
186
|
+
"default",
|
|
187
|
+
"all"
|
|
188
|
+
]),
|
|
189
|
+
"--ignore-submodules": new Set([
|
|
190
|
+
"none",
|
|
191
|
+
"untracked",
|
|
192
|
+
"dirty",
|
|
193
|
+
"all"
|
|
194
|
+
])
|
|
195
|
+
};
|
|
196
|
+
const MAX_MCP_CONTEXT_LINES = 1e5;
|
|
197
|
+
function boundedMcpInteger(name, value, maximum) {
|
|
198
|
+
if (!/^\d+$/.test(value)) throw new Error(`${name} requires a non-negative integer, received ${JSON.stringify(value)}.`);
|
|
199
|
+
const parsed = Number(value);
|
|
200
|
+
if (!Number.isSafeInteger(parsed) || parsed > maximum) throw new Error(`${name} requires a safe integer from 0 to ${maximum}, received ${JSON.stringify(value)}.`);
|
|
201
|
+
return parsed;
|
|
202
|
+
}
|
|
203
|
+
function validateMcpDiffValue(name, value) {
|
|
204
|
+
if (value.includes("\0")) throw new Error(`Value for ${name} must not contain NUL bytes.`);
|
|
205
|
+
const allowed = SAFE_MCP_ENUM_VALUES[name];
|
|
206
|
+
if (allowed && !allowed.has(value)) throw new Error(`Unsupported value ${JSON.stringify(value)} for ${name}. Expected one of: ${[...allowed].join(", ")}.`);
|
|
207
|
+
if (name === "--unified" || name === "--inter-hunk-context" || name === "-U") boundedMcpInteger(name, value, MAX_MCP_CONTEXT_LINES);
|
|
208
|
+
if ([
|
|
209
|
+
"--find-copies",
|
|
210
|
+
"--find-renames",
|
|
211
|
+
"-C",
|
|
212
|
+
"-M"
|
|
213
|
+
].includes(name)) boundedMcpInteger(name, value, 100);
|
|
214
|
+
if (name === "--break-rewrites" || name === "-B") {
|
|
215
|
+
const parts = value.split("/");
|
|
216
|
+
const invalidPart = parts.some((part) => {
|
|
217
|
+
if (!/^\d+$/.test(part)) return true;
|
|
218
|
+
const parsed = Number(part);
|
|
219
|
+
return !Number.isSafeInteger(parsed) || parsed > 100;
|
|
220
|
+
});
|
|
221
|
+
if (parts.length > 2 || invalidPart) throw new Error(`${name} requires one or two slash-separated integers from 0 to 100, received ${JSON.stringify(value)}.`);
|
|
222
|
+
}
|
|
223
|
+
if (name === "--diff-filter" && !/^(?:[ACDMRTUXBacdmrtuxb]+\*?|\*)$/.test(value)) throw new Error(`${name} contains unsupported filter letters: ${JSON.stringify(value)}.`);
|
|
224
|
+
}
|
|
225
|
+
function attachedShortOption(arg) {
|
|
226
|
+
const match = /^(-[UCMBGS])(.+)$/.exec(arg);
|
|
227
|
+
return match ? {
|
|
228
|
+
name: match[1],
|
|
229
|
+
value: match[2]
|
|
230
|
+
} : null;
|
|
231
|
+
}
|
|
232
|
+
/** Validate MCP-controlled git arguments before the general CLI parser sees them. */
|
|
233
|
+
function validateMcpDiffArgs(diffArgs) {
|
|
234
|
+
let pathsOnly = false;
|
|
235
|
+
for (let index = 0; index < diffArgs.length; index += 1) {
|
|
236
|
+
const arg = diffArgs[index];
|
|
237
|
+
if (arg.includes("\0")) throw new Error("diffArgs must not contain NUL bytes.");
|
|
238
|
+
if (pathsOnly) continue;
|
|
239
|
+
if (arg === "--") {
|
|
240
|
+
pathsOnly = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!arg.startsWith("-")) continue;
|
|
244
|
+
if (SAFE_MCP_BOOLEAN_DIFF_ARGS.has(arg)) continue;
|
|
245
|
+
const attached = attachedShortOption(arg);
|
|
246
|
+
if (attached) {
|
|
247
|
+
validateMcpDiffValue(attached.name, attached.value);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const equals = arg.indexOf("=");
|
|
251
|
+
const name = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
252
|
+
if (SAFE_MCP_VALUE_DIFF_ARGS.has(name)) {
|
|
253
|
+
let value;
|
|
254
|
+
if (equals < 0) {
|
|
255
|
+
value = diffArgs[index + 1];
|
|
256
|
+
if (value === void 0 || value === "--" || value.startsWith("-")) throw new Error(`Safe diff option ${name} requires a value.`);
|
|
257
|
+
index += 1;
|
|
258
|
+
} else if (arg.slice(equals + 1).length === 0) throw new Error(`Safe diff option ${name} requires a value.`);
|
|
259
|
+
else value = arg.slice(equals + 1);
|
|
260
|
+
validateMcpDiffValue(name, value);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
throw new Error(`Unsafe or unsupported diff argument ${JSON.stringify(arg)}. start_review_session accepts only revision/path scope, filtering, whitespace, context, and rename-detection options; output, external-driver, and diffing runtime flags are forbidden.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function normalizeMcpDiffArgs(diffArgs) {
|
|
267
|
+
const normalized = [];
|
|
268
|
+
let pathsOnly = false;
|
|
269
|
+
for (let index = 0; index < diffArgs.length; index += 1) {
|
|
270
|
+
const arg = diffArgs[index];
|
|
271
|
+
if (arg === "--") pathsOnly = true;
|
|
272
|
+
if (!pathsOnly && SAFE_MCP_VALUE_DIFF_ARGS.has(arg)) {
|
|
273
|
+
const separator = arg.startsWith("--") ? "=" : "";
|
|
274
|
+
normalized.push(`${arg}${separator}${diffArgs[index + 1]}`);
|
|
275
|
+
index += 1;
|
|
276
|
+
} else normalized.push(arg);
|
|
277
|
+
}
|
|
278
|
+
return normalized;
|
|
279
|
+
}
|
|
280
|
+
function expectedBuiltMcpArg(arg) {
|
|
281
|
+
const aliases = {
|
|
282
|
+
"--cached": "--staged",
|
|
283
|
+
"-b": "--ignore-space-change",
|
|
284
|
+
"-w": "--ignore-all-space",
|
|
285
|
+
"-W": "--function-context"
|
|
286
|
+
};
|
|
287
|
+
if (aliases[arg]) return aliases[arg];
|
|
288
|
+
const attached = attachedShortOption(arg);
|
|
289
|
+
if (attached) {
|
|
290
|
+
const { name, value } = attached;
|
|
291
|
+
if (name === "-U") return `--unified=${Number(value)}`;
|
|
292
|
+
if (name === "-C") return Number(value) === 40 ? "-C" : `-C${Number(value)}`;
|
|
293
|
+
if (name === "-M") return Number(value) === 50 ? "-M" : `-M${Number(value)}`;
|
|
294
|
+
return arg;
|
|
295
|
+
}
|
|
296
|
+
const equals = arg.indexOf("=");
|
|
297
|
+
if (equals < 0) return SAFE_MCP_BOOLEAN_DIFF_ARGS.has(arg) ? arg : null;
|
|
298
|
+
const name = arg.slice(0, equals);
|
|
299
|
+
const value = arg.slice(equals + 1);
|
|
300
|
+
if (name === "--find-copies") return Number(value) === 40 ? "-C" : `-C${Number(value)}`;
|
|
301
|
+
if (name === "--find-renames") return Number(value) === 50 ? "-M" : `-M${Number(value)}`;
|
|
302
|
+
if (name === "--break-rewrites") return `-B${value}`;
|
|
303
|
+
return arg;
|
|
304
|
+
}
|
|
305
|
+
function assertMcpModifiersAreEmitted(normalizedArgs, parsed) {
|
|
306
|
+
const builtArgs = buildGitDiffArgs(parsed);
|
|
307
|
+
const separator = normalizedArgs.indexOf("--");
|
|
308
|
+
const scopeArgs = separator < 0 ? normalizedArgs : normalizedArgs.slice(0, separator);
|
|
309
|
+
for (const arg of scopeArgs) {
|
|
310
|
+
if (!arg.startsWith("-")) continue;
|
|
311
|
+
const expected = expectedBuiltMcpArg(arg);
|
|
312
|
+
if (expected && !builtArgs.includes(expected)) throw new Error(`Unsupported diff argument ${JSON.stringify(arg)}: it is not preserved in the final Git diff arguments.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function validateMcpModifierAnchoring(diffArgs, parsed) {
|
|
316
|
+
if (parsed.revisions.length > 0 || parsed.pathspecs.length > 0) return;
|
|
317
|
+
const baseline = new Set([
|
|
318
|
+
"--staged",
|
|
319
|
+
"--cached",
|
|
320
|
+
"--patch",
|
|
321
|
+
"-p",
|
|
322
|
+
"--no-ext-diff",
|
|
323
|
+
"--no-textconv"
|
|
324
|
+
]);
|
|
325
|
+
const modifiers = diffArgs.slice(0, Math.max(0, diffArgs.indexOf("--") === -1 ? diffArgs.length : diffArgs.indexOf("--"))).filter((arg) => arg.startsWith("-") && !baseline.has(arg));
|
|
326
|
+
if (modifiers.length > 0) throw new Error(`Diff modifiers ${modifiers.map((arg) => JSON.stringify(arg)).join(", ")} require a revision or a pathspec after --. Without that anchor diffing uses its baseline working-tree engine, which cannot honor these modifiers.`);
|
|
327
|
+
}
|
|
328
|
+
function createMcpServer(options) {
|
|
329
|
+
const repoRoot = resolveMcpRepository(options.repoRoot, true);
|
|
330
|
+
const startServerFn = options.startServerFn ?? startServer;
|
|
331
|
+
const now = options.now ?? Date.now;
|
|
332
|
+
const readLock = options.readLock ?? readServerLock;
|
|
333
|
+
const writeLock = options.writeLock ?? writeServerLock;
|
|
334
|
+
const removeLock = options.removeLock ?? removeServerLock;
|
|
335
|
+
const lockIsAlive = options.lockIsAlive ?? isLockAlive;
|
|
336
|
+
const acquireStartupLease = options.acquireStartupLease ?? acquireServerStartupLease;
|
|
337
|
+
const ownerId = options.ownerId ?? randomUUID();
|
|
338
|
+
const defaultClientDir = existsSync(resolve(moduleDirectory, "client")) ? resolve(moduleDirectory, "client") : resolve(repoRoot, "dist/client");
|
|
339
|
+
const clientDir = options.clientDir ?? defaultClientDir;
|
|
340
|
+
const instructions = `diffing is bound to ${repoRoot} for this connection. First call review_session_status. If no web session is running, call start_review_session; it is safe to retry and never replaces a user session. For code review, call get_diff, create_comment as needed, then await_review for the human handoff. For plan review, call submit_plan then await_plan_review. A changes-requested verdict means revise and resubmit the same planId; rejected means stop; approved means proceed. Wait tools return released or timeout with a mode; timeout is not failure and the same wait tool may be called again. All networking remains local.`;
|
|
341
|
+
const server = new McpServer({
|
|
342
|
+
name: "diffing",
|
|
343
|
+
version: MCP_VERSION
|
|
344
|
+
}, { instructions });
|
|
345
|
+
let reviewCursor = null;
|
|
346
|
+
let planCursor = null;
|
|
347
|
+
function liveLock() {
|
|
348
|
+
const lock = readLock(repoRoot);
|
|
349
|
+
return lock && lockIsAlive(lock, repoRoot) ? lock : null;
|
|
350
|
+
}
|
|
351
|
+
function ensureReusableLock(lock) {
|
|
352
|
+
if (lock.owner === "mcp" && (lock.pid !== process.pid || lock.ownerId !== ownerId)) throw new Error(`A different diffing MCP connection (${lock.pid}/${lock.ownerId ?? "legacy"}) owns this repository session. Its web server lifecycle is tied to that MCP connection; stop it or wait for it to exit, then retry.`);
|
|
353
|
+
if (!isLoopbackHost(lock.host)) throw new Error(`The active diffing session is bound to non-loopback host ${JSON.stringify(lock.host)}. MCP refuses LAN/remote review URLs; end it and call start_review_session for a loopback-only session.`);
|
|
354
|
+
}
|
|
355
|
+
function requireWebSession() {
|
|
356
|
+
const lock = liveLock();
|
|
357
|
+
if (!lock) throw new Error(`No diffing web review session is running for ${repoRoot}. Call start_review_session, then retry this tool.`);
|
|
358
|
+
ensureReusableLock(lock);
|
|
359
|
+
const mode = lock.mode ?? "web";
|
|
360
|
+
if (mode !== "web") throw new Error(`The active diffing session is ${mode}, not a local web review. MCP local-review tools refuse TUI and GitHub PR stores; end it and call start_review_session.`);
|
|
361
|
+
const url = lockUrl(lock);
|
|
362
|
+
if (!url) throw new Error("The active diffing session is TUI-only. Start a web review session before using MCP review tools.");
|
|
363
|
+
return {
|
|
364
|
+
lock,
|
|
365
|
+
url,
|
|
366
|
+
identity: `${lock.pid}:${lock.startedAt}:${lock.port}:${lock.ownerId ?? ""}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function requireBaseUrl() {
|
|
370
|
+
return requireWebSession().url;
|
|
371
|
+
}
|
|
372
|
+
async function seedReviewCursor(session, signal) {
|
|
373
|
+
if (reviewCursor?.identity === session.identity) return reviewCursor.round;
|
|
374
|
+
const status = await requestJson(session.url, "/api/review/status", { signal });
|
|
375
|
+
reviewCursor = {
|
|
376
|
+
identity: session.identity,
|
|
377
|
+
round: Math.max(0, (status.round ?? 0) - 1)
|
|
378
|
+
};
|
|
379
|
+
return reviewCursor.round;
|
|
380
|
+
}
|
|
381
|
+
async function seedPlanCursor(session, signal, force = false) {
|
|
382
|
+
if (!force && planCursor?.identity === session.identity) return planCursor.round;
|
|
383
|
+
const status = await requestJson(session.url, "/api/plan-review/status", { signal });
|
|
384
|
+
planCursor = {
|
|
385
|
+
identity: session.identity,
|
|
386
|
+
round: force ? status.round ?? 0 : Math.max(0, (status.round ?? 0) - 1)
|
|
387
|
+
};
|
|
388
|
+
return planCursor.round;
|
|
389
|
+
}
|
|
390
|
+
function sessionStatus() {
|
|
391
|
+
const lock = liveLock();
|
|
392
|
+
if (!lock) return {
|
|
393
|
+
repository: repoRoot,
|
|
394
|
+
serverState: "not-running",
|
|
395
|
+
mode: "none",
|
|
396
|
+
url: null,
|
|
397
|
+
managedBy: null,
|
|
398
|
+
diffArgs: [],
|
|
399
|
+
nextAction: "Call start_review_session, then get_diff."
|
|
400
|
+
};
|
|
401
|
+
const mode = lock.mode ?? "web";
|
|
402
|
+
const url = lockUrl(lock);
|
|
403
|
+
const inaccessible = mode !== "tui" && url === null;
|
|
404
|
+
return {
|
|
405
|
+
repository: repoRoot,
|
|
406
|
+
serverState: "running",
|
|
407
|
+
mode,
|
|
408
|
+
url,
|
|
409
|
+
managedBy: lock.owner === "mcp" ? "mcp" : "user",
|
|
410
|
+
diffArgs: lock.diffArgs ?? [],
|
|
411
|
+
nextAction: inaccessible ? "This session is not loopback-only, so MCP will not connect to it. End it manually and call start_review_session." : mode === "tui" ? "A TUI session cannot serve MCP tools; stop it manually and call start_review_session." : mode === "gh-pr" ? "This is a GitHub PR session; use the GitHub review workflow or start a local session after it ends." : "Call get_diff to inspect changes, or use a plan-review tool."
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
server.registerTool("review_session_status", {
|
|
415
|
+
title: "Get review session status",
|
|
416
|
+
description: "Inspect the repository binding and active diffing session. Works when no web server is running; use its nextAction before other tools.",
|
|
417
|
+
inputSchema: {},
|
|
418
|
+
outputSchema: {
|
|
419
|
+
repository: z.string(),
|
|
420
|
+
serverState: z.enum(["running", "not-running"]),
|
|
421
|
+
mode: z.enum([
|
|
422
|
+
"none",
|
|
423
|
+
"web",
|
|
424
|
+
"gh-pr",
|
|
425
|
+
"tui"
|
|
426
|
+
]),
|
|
427
|
+
url: z.string().nullable(),
|
|
428
|
+
managedBy: z.enum(["mcp", "user"]).nullable(),
|
|
429
|
+
diffArgs: z.array(z.string()),
|
|
430
|
+
nextAction: z.string()
|
|
431
|
+
},
|
|
432
|
+
annotations: READ_ONLY
|
|
433
|
+
}, async () => {
|
|
434
|
+
const status = sessionStatus();
|
|
435
|
+
return textResult(status.serverState === "running" ? `diffing ${status.mode} session is running for ${repoRoot}${status.url ? ` at ${status.url}` : ""}. ${status.nextAction}` : `No diffing web session is running for ${repoRoot}. ${status.nextAction}`, { ...status });
|
|
436
|
+
});
|
|
437
|
+
let startQueue = Promise.resolve();
|
|
438
|
+
server.registerTool("start_review_session", {
|
|
439
|
+
title: "Start or reuse a local review session",
|
|
440
|
+
description: "Idempotently reuse a matching web review session or start a headless loopback-only session on an OS-selected port. Modifiers require a revision or pathspec anchor so the custom engine honors them. Arguments are never passed to a shell; incompatible sessions are reported, never stopped.",
|
|
441
|
+
inputSchema: { diffArgs: z.array(z.string()).optional().describe("Optional git-diff arguments, for example [\"--staged\"] or [\"main...HEAD\", \"--\", \"src/\"].") },
|
|
442
|
+
outputSchema: {
|
|
443
|
+
status: z.enum(["started", "reused"]),
|
|
444
|
+
repository: z.string(),
|
|
445
|
+
url: z.string(),
|
|
446
|
+
mode: z.literal("web"),
|
|
447
|
+
managedBy: z.enum(["mcp", "user"]),
|
|
448
|
+
diffArgs: z.array(z.string()),
|
|
449
|
+
nextAction: z.string()
|
|
450
|
+
},
|
|
451
|
+
annotations: IDEMPOTENT_MUTATION
|
|
452
|
+
}, async ({ diffArgs = [] }) => {
|
|
453
|
+
validateMcpDiffArgs(diffArgs);
|
|
454
|
+
const normalizedArgs = normalizeMcpDiffArgs(diffArgs);
|
|
455
|
+
const parsed = parseDiffOptions(normalizedArgs);
|
|
456
|
+
validateMcpModifierAnchoring(diffArgs, parsed);
|
|
457
|
+
parsed.outputMode = "web";
|
|
458
|
+
parsed.host = "127.0.0.1";
|
|
459
|
+
parsed.port = void 0;
|
|
460
|
+
parsed.noOpen = true;
|
|
461
|
+
parsed.noExtDiff = true;
|
|
462
|
+
parsed.textconv = false;
|
|
463
|
+
parsed.extDiff = void 0;
|
|
464
|
+
parsed.outputFormat = void 0;
|
|
465
|
+
parsed.outputFile = void 0;
|
|
466
|
+
parsed.exitCode = false;
|
|
467
|
+
parsed.quiet = false;
|
|
468
|
+
parsed.check = false;
|
|
469
|
+
parsed.binary = false;
|
|
470
|
+
assertMcpModifiersAreEmitted(normalizedArgs, parsed);
|
|
471
|
+
const requestedScope = diffScopeKey(parsed);
|
|
472
|
+
const operation = startQueue.then(async () => {
|
|
473
|
+
const reuse = (existing) => {
|
|
474
|
+
ensureReusableLock(existing);
|
|
475
|
+
const mode = existing.mode ?? "web";
|
|
476
|
+
if (mode !== "web") throw new Error(`A live ${mode} diffing session already owns this repository. diffing will not replace or stop it; end that session manually before starting a local web review.`);
|
|
477
|
+
if (!sameScope(existing, requestedScope, diffArgs)) throw new Error(`A live diffing web session already shows a different diff scope. Requested arguments: ${JSON.stringify(diffArgs)}. Use the existing session or end it manually; diffing will not replace it.`);
|
|
478
|
+
const url = lockUrl(existing);
|
|
479
|
+
if (!url) throw new Error("The active diffing web session has no safe loopback URL.");
|
|
480
|
+
return {
|
|
481
|
+
status: "reused",
|
|
482
|
+
repository: repoRoot,
|
|
483
|
+
url,
|
|
484
|
+
mode: "web",
|
|
485
|
+
managedBy: existing.owner === "mcp" ? "mcp" : "user",
|
|
486
|
+
diffArgs: existing.diffArgs ?? diffArgs,
|
|
487
|
+
nextAction: "Call get_diff to inspect the active diff."
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
const beforeLease = liveLock();
|
|
491
|
+
if (beforeLease) return reuse(beforeLease);
|
|
492
|
+
const lease = acquireStartupLease(repoRoot, ownerId);
|
|
493
|
+
if (!lease) throw new Error("Another diffing process is starting a review session for this repository. Retry start_review_session after that startup completes.");
|
|
494
|
+
try {
|
|
495
|
+
const afterLease = liveLock();
|
|
496
|
+
if (afterLease) return reuse(afterLease);
|
|
497
|
+
const lock = {
|
|
498
|
+
port: (await startServerFn({
|
|
499
|
+
port: 0,
|
|
500
|
+
host: "127.0.0.1",
|
|
501
|
+
clientDir,
|
|
502
|
+
diffOpts: parsed
|
|
503
|
+
})).port,
|
|
504
|
+
host: "127.0.0.1",
|
|
505
|
+
pid: process.pid,
|
|
506
|
+
repoRoot,
|
|
507
|
+
startedAt: now(),
|
|
508
|
+
version: MCP_VERSION,
|
|
509
|
+
mode: "web",
|
|
510
|
+
scope: requestedScope,
|
|
511
|
+
diffArgs: [...diffArgs],
|
|
512
|
+
owner: "mcp",
|
|
513
|
+
ownerId
|
|
514
|
+
};
|
|
515
|
+
try {
|
|
516
|
+
writeLock(lock);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const persisted = readLock(repoRoot);
|
|
519
|
+
if (persisted?.owner === "mcp" && persisted.ownerId === ownerId) removeLock(repoRoot);
|
|
520
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
521
|
+
throw new Error(`The review server bound locally but its discovery lock could not be written: ${detail}. No MCP session was claimed; retry after fixing lock storage. The unadvertised loopback listener will close when this MCP process exits.`);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
status: "started",
|
|
525
|
+
repository: repoRoot,
|
|
526
|
+
url: lockUrl(lock),
|
|
527
|
+
mode: "web",
|
|
528
|
+
managedBy: "mcp",
|
|
529
|
+
diffArgs: [...diffArgs],
|
|
530
|
+
nextAction: "Call get_diff to inspect the active diff."
|
|
531
|
+
};
|
|
532
|
+
} finally {
|
|
533
|
+
lease.release();
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
startQueue = operation.then(() => void 0, () => void 0);
|
|
537
|
+
const result = await operation;
|
|
538
|
+
return textResult(`${result.status === "started" ? "Started" : "Reused"} review session at ${result.url}.`, result);
|
|
539
|
+
});
|
|
540
|
+
server.registerTool("get_diff", {
|
|
541
|
+
title: "Get the active local diff",
|
|
542
|
+
description: "Fetch the complete patch and basic repository metadata from the active local web review. Start or locate the session first.",
|
|
543
|
+
inputSchema: {},
|
|
544
|
+
outputSchema: {
|
|
545
|
+
patch: z.string(),
|
|
546
|
+
repoName: z.string(),
|
|
547
|
+
branch: z.string(),
|
|
548
|
+
customMode: z.boolean(),
|
|
549
|
+
binaryFiles: z.array(z.object({
|
|
550
|
+
path: z.string(),
|
|
551
|
+
type: z.enum([
|
|
552
|
+
"added",
|
|
553
|
+
"deleted",
|
|
554
|
+
"changed",
|
|
555
|
+
"untracked"
|
|
556
|
+
])
|
|
557
|
+
})),
|
|
558
|
+
tabSizeMap: z.record(z.string(), z.number()),
|
|
559
|
+
untrackedFiles: z.array(z.string()),
|
|
560
|
+
showMode: z.boolean().optional(),
|
|
561
|
+
commits: z.array(z.unknown()).optional(),
|
|
562
|
+
truncated: z.number().optional()
|
|
563
|
+
},
|
|
564
|
+
annotations: READ_ONLY
|
|
565
|
+
}, async () => {
|
|
566
|
+
const diff = await requestJson(requireBaseUrl(), "/api/diff");
|
|
567
|
+
const structured = {
|
|
568
|
+
patch: diff.patch,
|
|
569
|
+
repoName: diff.repoName,
|
|
570
|
+
branch: diff.branch,
|
|
571
|
+
customMode: diff.customMode,
|
|
572
|
+
binaryFiles: diff.binaryFiles,
|
|
573
|
+
tabSizeMap: diff.tabSizeMap,
|
|
574
|
+
untrackedFiles: diff.untrackedFiles,
|
|
575
|
+
...typeof diff.showMode === "boolean" ? { showMode: diff.showMode } : {},
|
|
576
|
+
...Array.isArray(diff.commits) ? { commits: diff.commits } : {},
|
|
577
|
+
...typeof diff.truncated === "number" ? { truncated: diff.truncated } : {}
|
|
578
|
+
};
|
|
579
|
+
return textResult(diff.patch || "(The active diff is empty.)", structured);
|
|
580
|
+
});
|
|
581
|
+
server.registerTool("create_comment", {
|
|
582
|
+
title: "Create an inline review comment",
|
|
583
|
+
description: "Create a local inline comment on an exact line from get_diff. side is additions for +/context in the new file and deletions for a removed line.",
|
|
584
|
+
inputSchema: {
|
|
585
|
+
filePath: z.string().min(1).describe("Repository-relative file path exactly as shown in the patch."),
|
|
586
|
+
side: z.enum(["deletions", "additions"]).describe("Which side of the patch contains the target line."),
|
|
587
|
+
lineNumber: z.number().int().positive().describe("Target line number on the selected side."),
|
|
588
|
+
startLineNumber: z.number().int().positive().optional().describe("Optional first line for a multiline comment."),
|
|
589
|
+
lineContent: z.string().describe("Target line text, used to preserve context in the review UI."),
|
|
590
|
+
body: z.string().min(1).describe("Actionable review comment in Markdown.")
|
|
591
|
+
},
|
|
592
|
+
outputSchema: {
|
|
593
|
+
status: z.literal("created"),
|
|
594
|
+
comment: commentSchema
|
|
595
|
+
},
|
|
596
|
+
annotations: MUTATING
|
|
597
|
+
}, async (input) => {
|
|
598
|
+
const comment = await requestJson(requireBaseUrl(), "/api/comments", {
|
|
599
|
+
method: "POST",
|
|
600
|
+
headers: { "Content-Type": "application/json" },
|
|
601
|
+
body: JSON.stringify(input)
|
|
602
|
+
});
|
|
603
|
+
return textResult(`Created inline comment ${comment.id} on ${comment.filePath}:${comment.lineNumber}.`, {
|
|
604
|
+
status: "created",
|
|
605
|
+
comment
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
server.registerTool("await_review", {
|
|
609
|
+
title: "Wait for code review handoff",
|
|
610
|
+
description: "Wait for the human to send a code-review round. Returns status=released or status=timeout and always includes mode. Timeout is expected and safe to retry; progress notifications are sent between long polls.",
|
|
611
|
+
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe("Total wait budget in seconds; defaults to 570.") },
|
|
612
|
+
outputSchema: {
|
|
613
|
+
status: z.enum(["released", "timeout"]),
|
|
614
|
+
mode: z.enum(["standard", "comment-only"]),
|
|
615
|
+
round: z.number(),
|
|
616
|
+
openCount: z.number().optional(),
|
|
617
|
+
decision: z.enum([
|
|
618
|
+
"approved",
|
|
619
|
+
"changes-requested",
|
|
620
|
+
"rejected",
|
|
621
|
+
"comment-only"
|
|
622
|
+
]).optional(),
|
|
623
|
+
comments: z.array(commentSchema).optional(),
|
|
624
|
+
nextAction: z.string()
|
|
625
|
+
},
|
|
626
|
+
annotations: AWAIT
|
|
627
|
+
}, async ({ timeoutSeconds }, extra) => {
|
|
628
|
+
const session = requireWebSession();
|
|
629
|
+
const base = session.url;
|
|
630
|
+
const budgetMs = (timeoutSeconds ?? 570) * 1e3;
|
|
631
|
+
const progressToken = extra?._meta?.progressToken;
|
|
632
|
+
let sinceRound = await seedReviewCursor(session, extra?.signal);
|
|
633
|
+
const deadline = Date.now() + budgetMs;
|
|
634
|
+
let cycle = 0;
|
|
635
|
+
while (Date.now() < deadline) {
|
|
636
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
637
|
+
const result = await requestJson(base, `/api/review/await?timeoutMs=${Math.min(25e3, remaining)}&sinceRound=${sinceRound}`, { signal: extra?.signal });
|
|
638
|
+
if (result.status === "released") {
|
|
639
|
+
const payload = result.payload;
|
|
640
|
+
sinceRound = payload.round;
|
|
641
|
+
reviewCursor = {
|
|
642
|
+
identity: session.identity,
|
|
643
|
+
round: sinceRound
|
|
644
|
+
};
|
|
645
|
+
const structured = {
|
|
646
|
+
status: "released",
|
|
647
|
+
mode: payload.mode ?? "standard",
|
|
648
|
+
round: payload.round,
|
|
649
|
+
openCount: payload.openCount,
|
|
650
|
+
...payload.decision ? { decision: payload.decision } : {},
|
|
651
|
+
comments: payload.comments,
|
|
652
|
+
nextAction: payload.mode === "comment-only" ? "Reply to comments without editing files; resolve only comments the human considers addressed." : "Address open comments, reply with evidence, and resolve completed threads."
|
|
653
|
+
};
|
|
654
|
+
return textResult(payload.commentXml, structured);
|
|
655
|
+
}
|
|
656
|
+
sinceRound = result.round ?? sinceRound;
|
|
657
|
+
reviewCursor = {
|
|
658
|
+
identity: session.identity,
|
|
659
|
+
round: sinceRound
|
|
660
|
+
};
|
|
661
|
+
cycle += 1;
|
|
662
|
+
if (progressToken !== void 0) await extra.sendNotification({
|
|
663
|
+
method: "notifications/progress",
|
|
664
|
+
params: {
|
|
665
|
+
progressToken,
|
|
666
|
+
progress: cycle,
|
|
667
|
+
total: Math.max(cycle, Math.ceil(budgetMs / 25e3)),
|
|
668
|
+
message: "Still waiting for a code-review handoff; long poll completed and will retry."
|
|
669
|
+
}
|
|
670
|
+
}).catch(() => {});
|
|
671
|
+
}
|
|
672
|
+
const structured = {
|
|
673
|
+
status: "timeout",
|
|
674
|
+
mode: "standard",
|
|
675
|
+
round: sinceRound,
|
|
676
|
+
nextAction: "No review was sent within the wait budget. Call await_review again to keep waiting."
|
|
677
|
+
};
|
|
678
|
+
return textResult(structured.nextAction, structured);
|
|
679
|
+
});
|
|
680
|
+
server.registerTool("list_comments", {
|
|
681
|
+
title: "List code review comments",
|
|
682
|
+
description: "Fetch code-review comments as XML plus structured data. Use openOnly=true when addressing the current review round.",
|
|
683
|
+
inputSchema: { openOnly: z.boolean().optional().describe("Return only unresolved comments when true.") },
|
|
684
|
+
outputSchema: { comments: z.array(commentSchema) },
|
|
685
|
+
annotations: READ_ONLY
|
|
686
|
+
}, async ({ openOnly }) => {
|
|
687
|
+
const all = await requestJson(requireBaseUrl(), "/api/comments");
|
|
688
|
+
const comments = openOnly ? all.filter((comment) => comment.status === "open") : all;
|
|
689
|
+
return textResult(formatComments(comments), { comments });
|
|
690
|
+
});
|
|
691
|
+
server.registerTool("reply_to_comment", {
|
|
692
|
+
title: "Reply to a code review comment",
|
|
693
|
+
description: "Post an agent reply to an existing code-review thread. Include concise evidence of the answer or applied change.",
|
|
694
|
+
inputSchema: {
|
|
695
|
+
commentId: z.string().min(1).describe("Comment id from list_comments or await_review."),
|
|
696
|
+
body: z.string().min(1).describe("Reply body in Markdown."),
|
|
697
|
+
model: z.string().optional().describe("Optional agent/model identifier shown in the UI.")
|
|
698
|
+
},
|
|
699
|
+
outputSchema: {
|
|
700
|
+
status: z.literal("replied"),
|
|
701
|
+
commentId: z.string()
|
|
702
|
+
},
|
|
703
|
+
annotations: MUTATING
|
|
704
|
+
}, async ({ commentId, body, model }) => {
|
|
705
|
+
await requestJson(requireBaseUrl(), `/api/comments/${encodeURIComponent(commentId)}/replies`, {
|
|
706
|
+
method: "POST",
|
|
707
|
+
headers: { "Content-Type": "application/json" },
|
|
708
|
+
body: JSON.stringify({
|
|
709
|
+
body,
|
|
710
|
+
role: "agent",
|
|
711
|
+
model
|
|
712
|
+
})
|
|
713
|
+
});
|
|
714
|
+
return textResult(`Replied to ${commentId}.`, {
|
|
715
|
+
status: "replied",
|
|
716
|
+
commentId
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
server.registerTool("resolve_comment", {
|
|
720
|
+
title: "Resolve a code review comment",
|
|
721
|
+
description: "Mark a code-review thread resolved after its request is fully addressed. Safe to retry.",
|
|
722
|
+
inputSchema: { commentId: z.string().min(1).describe("Comment id to resolve.") },
|
|
723
|
+
outputSchema: {
|
|
724
|
+
status: z.literal("resolved"),
|
|
725
|
+
commentId: z.string()
|
|
726
|
+
},
|
|
727
|
+
annotations: IDEMPOTENT_MUTATION
|
|
728
|
+
}, async ({ commentId }) => {
|
|
729
|
+
await requestJson(requireBaseUrl(), `/api/comments/${encodeURIComponent(commentId)}`, {
|
|
730
|
+
method: "PUT",
|
|
731
|
+
headers: { "Content-Type": "application/json" },
|
|
732
|
+
body: JSON.stringify({ status: "resolved" })
|
|
733
|
+
});
|
|
734
|
+
return textResult(`Resolved ${commentId}.`, {
|
|
735
|
+
status: "resolved",
|
|
736
|
+
commentId
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
server.registerTool("submit_plan", {
|
|
740
|
+
title: "Submit or resubmit a plan",
|
|
741
|
+
description: "Submit Markdown for human plan review. To revise after changes-requested, pass the same planId so a new version is created.",
|
|
742
|
+
inputSchema: {
|
|
743
|
+
title: z.string().optional().describe("Human-readable plan title."),
|
|
744
|
+
body: z.string().min(1).describe("Complete Markdown plan body."),
|
|
745
|
+
source: z.string().optional().describe("Optional source filename or workflow label."),
|
|
746
|
+
model: z.string().optional().describe("Optional agent/model identifier."),
|
|
747
|
+
planId: z.string().optional().describe("Existing plan id when resubmitting a revised version.")
|
|
748
|
+
},
|
|
749
|
+
outputSchema: {
|
|
750
|
+
status: z.literal("submitted"),
|
|
751
|
+
planId: z.string(),
|
|
752
|
+
version: z.number(),
|
|
753
|
+
url: z.string()
|
|
754
|
+
},
|
|
755
|
+
annotations: MUTATING
|
|
756
|
+
}, async ({ title, body, source, model, planId }) => {
|
|
757
|
+
const session = requireWebSession();
|
|
758
|
+
const base = session.url;
|
|
759
|
+
await seedPlanCursor(session, void 0, true);
|
|
760
|
+
const plan = await requestJson(base, "/api/plans", {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers: { "Content-Type": "application/json" },
|
|
763
|
+
body: JSON.stringify({
|
|
764
|
+
id: planId,
|
|
765
|
+
title,
|
|
766
|
+
body,
|
|
767
|
+
source,
|
|
768
|
+
model
|
|
769
|
+
})
|
|
770
|
+
});
|
|
771
|
+
const url = `${base}/plan/${plan.id}`;
|
|
772
|
+
return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. Call await_plan_review.`, {
|
|
773
|
+
status: "submitted",
|
|
774
|
+
planId: plan.id,
|
|
775
|
+
version: plan.version,
|
|
776
|
+
url
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
server.registerTool("await_plan_review", {
|
|
780
|
+
title: "Wait for plan review verdict",
|
|
781
|
+
description: "Wait for a plan verdict. Returns status=released or status=timeout and always includes mode. Timeout is expected and safe to retry; progress notifications are sent between long polls.",
|
|
782
|
+
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe("Total wait budget in seconds; defaults to 570.") },
|
|
783
|
+
outputSchema: {
|
|
784
|
+
status: z.enum(["released", "timeout"]),
|
|
785
|
+
mode: z.enum(["standard", "comment-only"]),
|
|
786
|
+
round: z.number(),
|
|
787
|
+
planId: z.string().optional(),
|
|
788
|
+
decision: z.enum([
|
|
789
|
+
"pending",
|
|
790
|
+
"approved",
|
|
791
|
+
"changes-requested",
|
|
792
|
+
"rejected",
|
|
793
|
+
"comment-only"
|
|
794
|
+
]).optional(),
|
|
795
|
+
decisionComment: z.string().optional(),
|
|
796
|
+
openCommentCount: z.number().optional(),
|
|
797
|
+
plan: z.unknown().optional(),
|
|
798
|
+
nextAction: z.string()
|
|
799
|
+
},
|
|
800
|
+
annotations: AWAIT
|
|
801
|
+
}, async ({ timeoutSeconds }, extra) => {
|
|
802
|
+
const session = requireWebSession();
|
|
803
|
+
const base = session.url;
|
|
804
|
+
const budgetMs = (timeoutSeconds ?? 570) * 1e3;
|
|
805
|
+
const progressToken = extra?._meta?.progressToken;
|
|
806
|
+
let sinceRound = await seedPlanCursor(session, extra?.signal);
|
|
807
|
+
const deadline = Date.now() + budgetMs;
|
|
808
|
+
let cycle = 0;
|
|
809
|
+
while (Date.now() < deadline) {
|
|
810
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
811
|
+
const result = await requestJson(base, `/api/plan-review/await?timeoutMs=${Math.min(25e3, remaining)}&sinceRound=${sinceRound}`, { signal: extra?.signal });
|
|
812
|
+
if (result.status === "released") {
|
|
813
|
+
const payload = result.payload;
|
|
814
|
+
sinceRound = payload.round;
|
|
815
|
+
planCursor = {
|
|
816
|
+
identity: session.identity,
|
|
817
|
+
round: sinceRound
|
|
818
|
+
};
|
|
819
|
+
const nextAction = payload.mode === "comment-only" ? "Reply to plan comments without changing implementation files." : payload.decision === "approved" ? "Proceed with the approved plan." : payload.decision === "changes-requested" ? "Revise the plan and call submit_plan with the same planId." : "Stop; the plan was rejected.";
|
|
820
|
+
const structured = {
|
|
821
|
+
status: "released",
|
|
822
|
+
mode: payload.mode ?? "standard",
|
|
823
|
+
round: payload.round,
|
|
824
|
+
planId: payload.planId,
|
|
825
|
+
decision: payload.decision,
|
|
826
|
+
...payload.decisionComment ? { decisionComment: payload.decisionComment } : {},
|
|
827
|
+
openCommentCount: payload.openCommentCount,
|
|
828
|
+
plan: payload.plan,
|
|
829
|
+
nextAction
|
|
830
|
+
};
|
|
831
|
+
return textResult(payload.reviewXml, structured);
|
|
832
|
+
}
|
|
833
|
+
sinceRound = result.round ?? sinceRound;
|
|
834
|
+
planCursor = {
|
|
835
|
+
identity: session.identity,
|
|
836
|
+
round: sinceRound
|
|
837
|
+
};
|
|
838
|
+
cycle += 1;
|
|
839
|
+
if (progressToken !== void 0) await extra.sendNotification({
|
|
840
|
+
method: "notifications/progress",
|
|
841
|
+
params: {
|
|
842
|
+
progressToken,
|
|
843
|
+
progress: cycle,
|
|
844
|
+
total: Math.max(cycle, Math.ceil(budgetMs / 25e3)),
|
|
845
|
+
message: "Still waiting for a plan verdict; long poll completed and will retry."
|
|
846
|
+
}
|
|
847
|
+
}).catch(() => {});
|
|
848
|
+
}
|
|
849
|
+
const structured = {
|
|
850
|
+
status: "timeout",
|
|
851
|
+
mode: "standard",
|
|
852
|
+
round: sinceRound,
|
|
853
|
+
nextAction: "No plan verdict arrived within the wait budget. Call await_plan_review again to keep waiting."
|
|
854
|
+
};
|
|
855
|
+
return textResult(structured.nextAction, structured);
|
|
856
|
+
});
|
|
857
|
+
server.registerTool("list_plans", {
|
|
858
|
+
title: "List submitted plans",
|
|
859
|
+
description: "List every submitted plan with its current verdict, version, and inline comments.",
|
|
860
|
+
inputSchema: {},
|
|
861
|
+
outputSchema: { plans: z.array(z.unknown()) },
|
|
862
|
+
annotations: READ_ONLY
|
|
863
|
+
}, async () => {
|
|
864
|
+
const plans = await requestJson(requireBaseUrl(), "/api/plans");
|
|
865
|
+
return textResult(plans.map((plan) => {
|
|
866
|
+
const open = (plan.comments ?? []).filter((comment) => comment.status === "open").length;
|
|
867
|
+
return `${plan.id} [${plan.decision}] v${plan.version} — ${open} open comment(s) — ${plan.title}`;
|
|
868
|
+
}).join("\n") || "No plans submitted yet.", { plans });
|
|
869
|
+
});
|
|
870
|
+
server.registerTool("get_plan", {
|
|
871
|
+
title: "Get a plan",
|
|
872
|
+
description: "Fetch one current plan as plan-review XML and structured data, including verdict and inline comments.",
|
|
873
|
+
inputSchema: { planId: z.string().min(1).describe("Plan id from submit_plan or list_plans.") },
|
|
874
|
+
outputSchema: { plan: z.unknown() },
|
|
875
|
+
annotations: READ_ONLY
|
|
876
|
+
}, async ({ planId }) => {
|
|
877
|
+
const plan = await requestJson(requireBaseUrl(), `/api/plans/${encodeURIComponent(planId)}`);
|
|
878
|
+
return textResult(formatPlanReview(plan), { plan });
|
|
879
|
+
});
|
|
880
|
+
server.registerTool("get_plan_versions", {
|
|
881
|
+
title: "List plan versions",
|
|
882
|
+
description: "List all submitted versions of a plan, oldest first.",
|
|
883
|
+
inputSchema: { planId: z.string().min(1).describe("Plan id to inspect.") },
|
|
884
|
+
outputSchema: { versions: z.array(z.unknown()) },
|
|
885
|
+
annotations: READ_ONLY
|
|
886
|
+
}, async ({ planId }) => {
|
|
887
|
+
const versions = await requestJson(requireBaseUrl(), `/api/plans/${encodeURIComponent(planId)}/versions`);
|
|
888
|
+
return textResult(versions.map((version) => {
|
|
889
|
+
const date = new Date(version.createdAt).toISOString().slice(0, 16).replace("T", " ");
|
|
890
|
+
return `v${version.version} — ${date} — ${version.title}`;
|
|
891
|
+
}).join("\n") || "No versions recorded.", { versions });
|
|
892
|
+
});
|
|
893
|
+
server.registerTool("get_plan_version", {
|
|
894
|
+
title: "Get a plan version",
|
|
895
|
+
description: "Fetch the current or a historical plan version as plan-review XML with version-anchored comments.",
|
|
896
|
+
inputSchema: {
|
|
897
|
+
planId: z.string().min(1).describe("Plan id to inspect."),
|
|
898
|
+
version: z.number().int().positive().optional().describe("Historical version number; omit for current.")
|
|
899
|
+
},
|
|
900
|
+
outputSchema: {
|
|
901
|
+
plan: z.unknown(),
|
|
902
|
+
version: z.unknown().optional(),
|
|
903
|
+
currentVersion: z.number().optional()
|
|
904
|
+
},
|
|
905
|
+
annotations: READ_ONLY
|
|
906
|
+
}, async ({ planId, version }) => {
|
|
907
|
+
const base = requireBaseUrl();
|
|
908
|
+
const encodedId = encodeURIComponent(planId);
|
|
909
|
+
const plan = await requestJson(base, `/api/plans/${encodedId}`);
|
|
910
|
+
if (version === void 0) return textResult(formatPlanReview(plan), { plan });
|
|
911
|
+
const data = await requestJson(base, `/api/plans/${encodedId}/versions/${version}`);
|
|
912
|
+
return textResult(formatPlanReview(plan, { viewingVersion: data.version.version }), {
|
|
913
|
+
plan,
|
|
914
|
+
version: data.version,
|
|
915
|
+
currentVersion: data.plan.currentVersion
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
async function findPlanForComment(base, commentId) {
|
|
919
|
+
return (await requestJson(base, "/api/plans")).find((plan) => (plan.comments ?? []).some((comment) => comment.id === commentId)) ?? null;
|
|
920
|
+
}
|
|
921
|
+
server.registerTool("reply_to_plan_comment", {
|
|
922
|
+
title: "Reply to a plan comment",
|
|
923
|
+
description: "Post an agent reply to an existing inline plan-review thread.",
|
|
924
|
+
inputSchema: {
|
|
925
|
+
commentId: z.string().min(1).describe("Plan comment id from get_plan or await_plan_review."),
|
|
926
|
+
body: z.string().min(1).describe("Reply body in Markdown."),
|
|
927
|
+
model: z.string().optional().describe("Optional agent/model identifier shown in the UI.")
|
|
928
|
+
},
|
|
929
|
+
outputSchema: {
|
|
930
|
+
status: z.literal("replied"),
|
|
931
|
+
commentId: z.string(),
|
|
932
|
+
planId: z.string()
|
|
933
|
+
},
|
|
934
|
+
annotations: MUTATING
|
|
935
|
+
}, async ({ commentId, body, model }) => {
|
|
936
|
+
const base = requireBaseUrl();
|
|
937
|
+
const plan = await findPlanForComment(base, commentId);
|
|
938
|
+
if (!plan) throw new Error(`Plan comment ${commentId} was not found. Refresh the plan before retrying.`);
|
|
939
|
+
await requestJson(base, `/api/plans/${encodeURIComponent(plan.id)}/comments/${encodeURIComponent(commentId)}/replies`, {
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers: { "Content-Type": "application/json" },
|
|
942
|
+
body: JSON.stringify({
|
|
943
|
+
body,
|
|
944
|
+
role: "agent",
|
|
945
|
+
model
|
|
946
|
+
})
|
|
947
|
+
});
|
|
948
|
+
return textResult(`Replied to plan comment ${commentId}.`, {
|
|
949
|
+
status: "replied",
|
|
950
|
+
commentId,
|
|
951
|
+
planId: plan.id
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
server.registerTool("resolve_plan_comment", {
|
|
955
|
+
title: "Resolve a plan comment",
|
|
956
|
+
description: "Mark an inline plan-review thread resolved after it is fully addressed. Safe to retry.",
|
|
957
|
+
inputSchema: { commentId: z.string().min(1).describe("Plan comment id to resolve.") },
|
|
958
|
+
outputSchema: {
|
|
959
|
+
status: z.literal("resolved"),
|
|
960
|
+
commentId: z.string(),
|
|
961
|
+
planId: z.string()
|
|
962
|
+
},
|
|
963
|
+
annotations: IDEMPOTENT_MUTATION
|
|
964
|
+
}, async ({ commentId }) => {
|
|
965
|
+
const base = requireBaseUrl();
|
|
966
|
+
const plan = await findPlanForComment(base, commentId);
|
|
967
|
+
if (!plan) throw new Error(`Plan comment ${commentId} was not found. Refresh the plan before retrying.`);
|
|
968
|
+
await requestJson(base, `/api/plans/${encodeURIComponent(plan.id)}/comments/${encodeURIComponent(commentId)}`, {
|
|
969
|
+
method: "PUT",
|
|
970
|
+
headers: { "Content-Type": "application/json" },
|
|
971
|
+
body: JSON.stringify({ status: "resolved" })
|
|
972
|
+
});
|
|
973
|
+
return textResult(`Resolved plan comment ${commentId}.`, {
|
|
974
|
+
status: "resolved",
|
|
975
|
+
commentId,
|
|
976
|
+
planId: plan.id
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
server.registerResource("agent-guide", "diffing://agent-guide", {
|
|
980
|
+
title: "diffing agent guide",
|
|
981
|
+
description: "Portable quick reference for the native code-review and plan-review workflows.",
|
|
982
|
+
mimeType: "text/markdown"
|
|
983
|
+
}, async () => ({ contents: [{
|
|
984
|
+
uri: "diffing://agent-guide",
|
|
985
|
+
mimeType: "text/markdown",
|
|
986
|
+
text: `# diffing agent guide
|
|
987
|
+
|
|
988
|
+
This MCP connection is immutably bound to \`${repoRoot}\`.
|
|
989
|
+
|
|
990
|
+
## Code review
|
|
991
|
+
1. Call \`review_session_status\`; call \`start_review_session\` if needed.
|
|
992
|
+
2. Call \`get_diff\` and inspect every changed file.
|
|
993
|
+
3. Use \`create_comment\` for actionable inline findings.
|
|
994
|
+
4. Use \`await_review\` for a human handoff; a timeout is safe to retry.
|
|
995
|
+
5. In \`comment-only\` mode, reply without editing files.
|
|
996
|
+
|
|
997
|
+
## Plan review
|
|
998
|
+
1. Start or reuse a review session.
|
|
999
|
+
2. Call \`submit_plan\`, then \`await_plan_review\`.
|
|
1000
|
+
3. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
|
|
1001
|
+
4. On \`approved\`, proceed; on \`rejected\`, stop.
|
|
1002
|
+
|
|
1003
|
+
All HTTP activity is loopback-only. MCP never terminates a user-owned session.`
|
|
1004
|
+
}] }));
|
|
1005
|
+
server.registerPrompt("review_local_changes", {
|
|
1006
|
+
title: "Review local changes with diffing",
|
|
1007
|
+
description: "Workflow prompt for inspecting a local diff and leaving inline review feedback.",
|
|
1008
|
+
argsSchema: { focus: z.string().optional().describe("Optional review focus such as security, correctness, or tests.") }
|
|
1009
|
+
}, async ({ focus }) => ({ messages: [{
|
|
1010
|
+
role: "user",
|
|
1011
|
+
content: {
|
|
1012
|
+
type: "text",
|
|
1013
|
+
text: `Use diffing to review the local changes in ${repoRoot}. Check review_session_status, start_review_session if needed, inspect get_diff, and create only actionable inline comments. Review every changed file${focus ? ` with special attention to ${focus}` : ""}.`
|
|
1014
|
+
}
|
|
1015
|
+
}] }));
|
|
1016
|
+
server.registerPrompt("submit_plan_for_review", {
|
|
1017
|
+
title: "Submit an implementation plan for review",
|
|
1018
|
+
description: "Workflow prompt for submitting a plan and acting on the human verdict.",
|
|
1019
|
+
argsSchema: { plan: z.string().describe("Complete Markdown implementation plan to submit.") }
|
|
1020
|
+
}, async ({ plan }) => ({ messages: [{
|
|
1021
|
+
role: "user",
|
|
1022
|
+
content: {
|
|
1023
|
+
type: "text",
|
|
1024
|
+
text: `Use diffing for plan review in ${repoRoot}. Start or reuse a review session, submit this plan, and await the verdict. On changes-requested revise and resubmit the same planId; on rejected stop; on approved proceed.
|
|
1025
|
+
|
|
1026
|
+
` + plan
|
|
1027
|
+
}
|
|
1028
|
+
}] }));
|
|
1029
|
+
return server;
|
|
1030
|
+
}
|
|
1031
|
+
async function startMcpServer(options = {}) {
|
|
1032
|
+
const repoRoot = resolveMcpRepository(options.repoPath ?? process.cwd(), options.repoPath !== void 0);
|
|
1033
|
+
process.chdir(repoRoot);
|
|
1034
|
+
const ownerId = randomUUID();
|
|
1035
|
+
const server = createMcpServer({
|
|
1036
|
+
repoRoot,
|
|
1037
|
+
ownerId
|
|
1038
|
+
});
|
|
1039
|
+
const cleanupOwnedSession = () => {
|
|
1040
|
+
removeServerLockIfOwned(repoRoot, process.pid, ownerId);
|
|
1041
|
+
};
|
|
1042
|
+
process.once("exit", cleanupOwnedSession);
|
|
1043
|
+
process.stdin.once("end", () => process.exit(0));
|
|
1044
|
+
await server.connect(new StdioServerTransport());
|
|
1045
|
+
}
|
|
1046
|
+
//#endregion
|
|
1047
|
+
export { startMcpServer };
|