diffing 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +41 -7
  2. package/dist/cli-agent-BatQgghr.mjs +499 -0
  3. package/dist/cli-agent-BwZE05CQ.mjs +499 -0
  4. package/dist/cli-agent-CKE1Cvor.mjs +513 -0
  5. package/dist/cli-agent-CLvCYpmH.mjs +499 -0
  6. package/dist/cli-agent-CX8Fa1Ph.mjs +499 -0
  7. package/dist/cli-agent-SILzesQP.mjs +513 -0
  8. package/dist/cli-agent-USzLoAit.mjs +499 -0
  9. package/dist/cli-gh-BV5az425.mjs +147 -0
  10. package/dist/cli-gh-Bo6fOHQR.mjs +147 -0
  11. package/dist/cli-gh-D1LoWJl-.mjs +147 -0
  12. package/dist/cli-gh-D1sIZ-jX.mjs +147 -0
  13. package/dist/cli-gh-DROAJlHi.mjs +147 -0
  14. package/dist/cli-gh-DiT8BWwu.mjs +147 -0
  15. package/dist/cli.mjs +97 -3381
  16. package/dist/client/assets/{angular-html-DIKVmS-w.js → angular-html-CDZ9SrQX.js} +1 -1
  17. package/dist/client/assets/{angular-ts-DK13mfBQ.js → angular-ts-CNMbVia5.js} +1 -1
  18. package/dist/client/assets/{apl-NVZUxqwo.js → apl-BqNxQgsm.js} +1 -1
  19. package/dist/client/assets/{arc-kijUuucY.js → arc-BrMp-_55.js} +1 -1
  20. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Dup-CzBS.js → architectureDiagram-3BPJPVTR-BIZJkdXp.js} +1 -1
  21. package/dist/client/assets/{astro-RPdCumat.js → astro-r55-Uy0e.js} +1 -1
  22. package/dist/client/assets/{blade-_ubz_KMD.js → blade-F8SZw05t.js} +1 -1
  23. package/dist/client/assets/{blockDiagram-GPEHLZMM-Cl0-G1MY.js → blockDiagram-GPEHLZMM-DlzFca7z.js} +1 -1
  24. package/dist/client/assets/{c-IEKcSTtW.js → c-DMmuyoXv.js} +1 -1
  25. package/dist/client/assets/{c4Diagram-AAUBKEIU-C3T6FV-K.js → c4Diagram-AAUBKEIU-BqJFYdcn.js} +1 -1
  26. package/dist/client/assets/channel-DVxVbqCb.js +1 -0
  27. package/dist/client/assets/{chunk-2J33WTMH-B5MFxFjX.js → chunk-2J33WTMH-CRN3t_Zt.js} +1 -1
  28. package/dist/client/assets/{chunk-3OPIFGDE-DWxCOZgI.js → chunk-3OPIFGDE-Bb8pGWXE.js} +1 -1
  29. package/dist/client/assets/{chunk-4BX2VUAB-DWdovlBC.js → chunk-4BX2VUAB--s6olL4E.js} +1 -1
  30. package/dist/client/assets/{chunk-55IACEB6-CFSaZijP.js → chunk-55IACEB6-C-iaNYfB.js} +1 -1
  31. package/dist/client/assets/{chunk-5ZQYHXKU-BH_cLkIj.js → chunk-5ZQYHXKU-Bvzi1CUr.js} +1 -1
  32. package/dist/client/assets/{chunk-727SXJPM-BbsJC2PT.js → chunk-727SXJPM-CjoyjZeg.js} +1 -1
  33. package/dist/client/assets/{chunk-AQP2D5EJ-tFAP6fC1.js → chunk-AQP2D5EJ-BJJOqjwm.js} +1 -1
  34. package/dist/client/assets/{chunk-BSJP7CBP-BGuhsdwy.js → chunk-BSJP7CBP-A0UL_0xV.js} +1 -1
  35. package/dist/client/assets/{chunk-CSCIHK7Q-DXSJWhsY.js → chunk-CSCIHK7Q-DW570vKT.js} +1 -1
  36. package/dist/client/assets/{chunk-FMBD7UC4-6vBylg_I.js → chunk-FMBD7UC4-CzNJ-Iy-.js} +1 -1
  37. package/dist/client/assets/{chunk-KSCS5N6A-C7iBFsUg.js → chunk-KSCS5N6A-DCbYTAZg.js} +1 -1
  38. package/dist/client/assets/{chunk-L5ZTLDWV-CRU3nvaK.js → chunk-L5ZTLDWV-BMdyyjjc.js} +1 -1
  39. package/dist/client/assets/{chunk-LZXEDZCA-6ppA-_4h.js → chunk-LZXEDZCA-BENQV-I4.js} +2 -2
  40. package/dist/client/assets/{chunk-ND2GUHAM-D1cTmgHe.js → chunk-ND2GUHAM-C1y3WzOx.js} +1 -1
  41. package/dist/client/assets/{chunk-NZK2D7GU-CpUab6v7.js → chunk-NZK2D7GU-BkBT6O_M.js} +1 -1
  42. package/dist/client/assets/{chunk-O5CBEL6O-cDxBycWe.js → chunk-O5CBEL6O-jdz4J9aA.js} +1 -1
  43. package/dist/client/assets/chunk-QZHKN3VN-mumapnEV.js +1 -0
  44. package/dist/client/assets/chunk-WU5MYG2G-Bsqs8ZKl.js +1 -0
  45. package/dist/client/assets/{chunk-XPW4576I-CI9GEEdy.js → chunk-XPW4576I-Bf8OI8wY.js} +1 -1
  46. package/dist/client/assets/classDiagram-4FO5ZUOK-CKaaw3uU.js +1 -0
  47. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BxwzeqDR.js +1 -0
  48. package/dist/client/assets/{cobol-BVhbHJgp.js → cobol-BanHX5pe.js} +1 -1
  49. package/dist/client/assets/{coffee-gY6asS_B.js → coffee-CACo0LEP.js} +1 -1
  50. package/dist/client/assets/{cose-bilkent-S5V4N54A-BOs7zxZa.js → cose-bilkent-S5V4N54A-7mNCsL0j.js} +1 -1
  51. package/dist/client/assets/{cpp-el3R6exS.js → cpp-4Kwtmkwn.js} +1 -1
  52. package/dist/client/assets/{crystal-D2_0btIo.js → crystal-D1kAaPsE.js} +1 -1
  53. package/dist/client/assets/{css-DtPe7dg4.js → css-CCMEBYEn.js} +1 -1
  54. package/dist/client/assets/{dagre-BM42HDAG-CR_0cBOz.js → dagre-BM42HDAG-BTEYZegB.js} +1 -1
  55. package/dist/client/assets/{diagram-2AECGRRQ-D9um0wp2.js → diagram-2AECGRRQ-Cj_XZNTN.js} +1 -1
  56. package/dist/client/assets/{diagram-5GNKFQAL-Dx0TEfBJ.js → diagram-5GNKFQAL-DVTFfYPd.js} +1 -1
  57. package/dist/client/assets/{diagram-KO2AKTUF-DjBHOuUD.js → diagram-KO2AKTUF-C7BU5Ncp.js} +1 -1
  58. package/dist/client/assets/{diagram-LMA3HP47-CK2MO5Gx.js → diagram-LMA3HP47-06rwT7mb.js} +1 -1
  59. package/dist/client/assets/{diagram-OG6HWLK6-CANQancN.js → diagram-OG6HWLK6-COEuKA9B.js} +1 -1
  60. package/dist/client/assets/{dist-DGlwcsQQ.js → dist-C-G1vI6B.js} +1 -1
  61. package/dist/client/assets/{edge-Ch_PYBsB.js → edge-BV-WbkC9.js} +1 -1
  62. package/dist/client/assets/{elixir-DEJrJATq.js → elixir-C6pMvu6H.js} +1 -1
  63. package/dist/client/assets/{elm-Brlko4-d.js → elm-D8LnUrCG.js} +1 -1
  64. package/dist/client/assets/{erDiagram-TEJ5UH35-Dnfrs6yk.js → erDiagram-TEJ5UH35-SPxPVg85.js} +1 -1
  65. package/dist/client/assets/{erb-DQPphNua.js → erb-C6ADBE0Q.js} +1 -1
  66. package/dist/client/assets/{flowDiagram-I6XJVG4X-BNMdFtjG.js → flowDiagram-I6XJVG4X-DyEhCtUv.js} +1 -1
  67. package/dist/client/assets/{ganttDiagram-6RSMTGT7-X-fPXz_A.js → ganttDiagram-6RSMTGT7-fj-zRHIk.js} +1 -1
  68. package/dist/client/assets/{git-rebase-BpS915SI.js → git-rebase-BhYZ4R3x.js} +1 -1
  69. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CRN_dRXM.js → gitGraphDiagram-PVQCEYII-CpUJh4CR.js} +1 -1
  70. package/dist/client/assets/{glimmer-js-DZDngVuD.js → glimmer-js-D2CVO-NF.js} +1 -1
  71. package/dist/client/assets/{glimmer-ts-DKNjJvyp.js → glimmer-ts-BjYisPeM.js} +1 -1
  72. package/dist/client/assets/{glsl-C-Y1tHoU.js → glsl-qP0lyfIY.js} +1 -1
  73. package/dist/client/assets/{graphql-DYVBNazO.js → graphql-BZE_lBur.js} +1 -1
  74. package/dist/client/assets/{hack-DewKAtLe.js → hack-C6KM9SLG.js} +1 -1
  75. package/dist/client/assets/{haml-CIA_AeXu.js → haml-BT3JKqL8.js} +1 -1
  76. package/dist/client/assets/{handlebars-Bade6JXX.js → handlebars-SHKYyRQ6.js} +1 -1
  77. package/dist/client/assets/{html-C4hhbuGp.js → html-CAc9HUGa.js} +1 -1
  78. package/dist/client/assets/{html-derivative-BXfztBtv.js → html-derivative-Cu6n_YRN.js} +1 -1
  79. package/dist/client/assets/{http-Cq90-CTl.js → http-o21PuOy8.js} +1 -1
  80. package/dist/client/assets/{hurl-DklQHYoa.js → hurl-ClrxMWQi.js} +1 -1
  81. package/dist/client/assets/{index-j9gqGQit.js → index-BPI8nJUS.js} +98 -98
  82. package/dist/client/assets/index-F_YfUisJ.css +1 -0
  83. package/dist/client/assets/{infoDiagram-5YYISTIA-Ht-AcvP4.js → infoDiagram-5YYISTIA-DDYGwUUs.js} +1 -1
  84. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-k_l70iFb.js → ishikawaDiagram-YF4QCWOH-Benmqbm6.js} +1 -1
  85. package/dist/client/assets/{java-BMUwLm1h.js → java-Bmlmy6B3.js} +1 -1
  86. package/dist/client/assets/{javascript-B8nPCUVp.js → javascript-Bdrc0lyI.js} +1 -1
  87. package/dist/client/assets/{jinja-CkxiRcB3.js → jinja-DBkct7nV.js} +1 -1
  88. package/dist/client/assets/{jison-CFnN5uQs.js → jison-Tb1xkShc.js} +1 -1
  89. package/dist/client/assets/{journeyDiagram-JHISSGLW-C98PUC9Y.js → journeyDiagram-JHISSGLW-CCkv1GnE.js} +1 -1
  90. package/dist/client/assets/{json-B7HNnjIx.js → json-DwLxGyui.js} +1 -1
  91. package/dist/client/assets/{jsx-BfVPuW3i.js → jsx-DWSSTqEB.js} +1 -1
  92. package/dist/client/assets/{julia-Bj0ETGjT.js → julia-LUN3XhcV.js} +1 -1
  93. package/dist/client/assets/{just-CqIe_c9k.js → just-C6LrD5tm.js} +1 -1
  94. package/dist/client/assets/{kanban-definition-UN3LZRKU-BE-uvH7w.js → kanban-definition-UN3LZRKU-CQSBf6Ys.js} +1 -1
  95. package/dist/client/assets/{latex-Cvy8E64e.js → latex-aaU-rirg.js} +1 -1
  96. package/dist/client/assets/{line-BpooNUgm.js → line-DG7hZYkR.js} +1 -1
  97. package/dist/client/assets/{linear-CM6ZoyCp.js → linear-A5s-ps1u.js} +1 -1
  98. package/dist/client/assets/{liquid-x09l8QqE.js → liquid-BhVOoUfG.js} +1 -1
  99. package/dist/client/assets/{lua-BSlDLjt5.js → lua-BTF9vmdD.js} +1 -1
  100. package/dist/client/assets/{marko-CoNMQlTn.js → marko-DlwBrpd_.js} +1 -1
  101. package/dist/client/assets/{mdc-BSzVESPu.js → mdc-H8mdhEag.js} +1 -1
  102. package/dist/client/assets/{mermaid-parser.core-BDXJMxHu.js → mermaid-parser.core-BKvEMhB3.js} +1 -1
  103. package/dist/client/assets/{mermaid.core-Dw-_vXTc.js → mermaid.core-BaDGp29W.js} +3 -3
  104. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BTxGT8rM.js → mindmap-definition-RKZ34NQL-BCrf7cJv.js} +1 -1
  105. package/dist/client/assets/{nginx-CBhJTj0i.js → nginx-DoaqXoFJ.js} +1 -1
  106. package/dist/client/assets/{nim-CtHbOBlO.js → nim-DqoAP4Np.js} +1 -1
  107. package/dist/client/assets/{perl-DLMzG3tu.js → perl-D1ou0isN.js} +1 -1
  108. package/dist/client/assets/{php-KomVR3Hz.js → php-DL6kQj3i.js} +1 -1
  109. package/dist/client/assets/{pieDiagram-4H26LBE5-BBUObAJW.js → pieDiagram-4H26LBE5-CF0X4ECG.js} +1 -1
  110. package/dist/client/assets/{pug-T4SRLx8P.js → pug-Bn3xDPVT.js} +1 -1
  111. package/dist/client/assets/{qml-DJdSRUmU.js → qml-CaA5S6hT.js} +1 -1
  112. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-B7vvauZv.js → quadrantDiagram-W4KKPZXB-A76pK4Cy.js} +1 -1
  113. package/dist/client/assets/{r-CwuTg9lK.js → r-DuUvfJBZ.js} +1 -1
  114. package/dist/client/assets/{razor-Ch4dgsGY.js → razor-DOv4xAWl.js} +1 -1
  115. package/dist/client/assets/{regexp-D2GTwJE0.js → regexp-DXetztjB.js} +1 -1
  116. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DpcNiR3Y.js → requirementDiagram-4Y6WPE33-Cju--fB8.js} +1 -1
  117. package/dist/client/assets/{rst-D78f0DyR.js → rst-CDn5EUee.js} +1 -1
  118. package/dist/client/assets/{ruby-BUcw6ITs.js → ruby-40qcnJX-.js} +1 -1
  119. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-ChzSQD2O.js → sankeyDiagram-5OEKKPKP-CvkQY1O-.js} +1 -1
  120. package/dist/client/assets/{sas-DoT2JDsj.js → sas-D3a9oRtu.js} +1 -1
  121. package/dist/client/assets/{scss-B6G_d-6b.js → scss-BJ8ZKq49.js} +1 -1
  122. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CzkdL8s7.js → sequenceDiagram-3UESZ5HK-DKU8lZlC.js} +1 -1
  123. package/dist/client/assets/{shellscript-DI252Bga.js → shellscript-yTgK8aYT.js} +1 -1
  124. package/dist/client/assets/{shellsession-BuO_s8FQ.js → shellsession-DrVaQSNg.js} +1 -1
  125. package/dist/client/assets/{soy-DZTZJTEE.js → soy-BasCadaZ.js} +1 -1
  126. package/dist/client/assets/{sql-C4isoJr_.js → sql-COJH6ALP.js} +1 -1
  127. package/dist/client/assets/{src-BMvz5HE8.js → src-BokBJ4oL.js} +1 -1
  128. package/dist/client/assets/{stata-Dhu6qITh.js → stata-Ble9lhMp.js} +1 -1
  129. package/dist/client/assets/{stateDiagram-AJRCARHV-DvYcrdap.js → stateDiagram-AJRCARHV-9IU9PT8A.js} +1 -1
  130. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ce46Lf8G.js +1 -0
  131. package/dist/client/assets/{surrealql-CSDpq5j3.js → surrealql-BZ5cQJLk.js} +1 -1
  132. package/dist/client/assets/{svelte-C4ITAvxr.js → svelte-CLa9l6VK.js} +1 -1
  133. package/dist/client/assets/{templ-BTgKA9-7.js → templ-DVKkE2Tb.js} +1 -1
  134. package/dist/client/assets/{tex-Dg4N9ndP.js → tex-BET3iISl.js} +1 -1
  135. package/dist/client/assets/{timeline-definition-PNZ67QCA-Djt5Lwk2.js → timeline-definition-PNZ67QCA-CkkYIhNz.js} +1 -1
  136. package/dist/client/assets/{ts-tags-BufYnYos.js → ts-tags-D5fPAs5W.js} +1 -1
  137. package/dist/client/assets/{tsx-DmkFP0sw.js → tsx-CfNzz9iB.js} +1 -1
  138. package/dist/client/assets/{twig-BJv_IWYK.js → twig-_Ml9CBGK.js} +1 -1
  139. package/dist/client/assets/{typescript-BKp05Qme.js → typescript-DMiV0dhX.js} +1 -1
  140. package/dist/client/assets/{vennDiagram-CIIHVFJN-DDVADTIa.js → vennDiagram-CIIHVFJN-HUv6L4Dt.js} +1 -1
  141. package/dist/client/assets/{vue-BsHXCXzh.js → vue-BcbmWIQt.js} +1 -1
  142. package/dist/client/assets/{vue-html-CCsH-lmo.js → vue-html-CcUgk6WA.js} +1 -1
  143. package/dist/client/assets/{vue-vine-B5P1JXxo.js → vue-vine-BbIBT3jy.js} +1 -1
  144. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-yeVKWvtX.js → wardleyDiagram-YWT4CUSO-avUfWkuC.js} +1 -1
  145. package/dist/client/assets/{xml-CFkhYff0.js → xml-9mtlBTUG.js} +1 -1
  146. package/dist/client/assets/{xsl-Dc5XRzm6.js → xsl-Dm8BN5j0.js} +1 -1
  147. package/dist/client/assets/{xychartDiagram-2RQKCTM6--8Ik-9v0.js → xychartDiagram-2RQKCTM6-BSZPOH7-.js} +1 -1
  148. package/dist/client/assets/{yaml-BTCCxCxi.js → yaml-Ccz1qKlL.js} +1 -1
  149. package/dist/client/index.html +2 -2
  150. package/dist/mcp-B6mCC7D3.mjs +1047 -0
  151. package/dist/mcp-BMTZLQuB.mjs +758 -0
  152. package/dist/mcp-BUF2nEL7.mjs +1036 -0
  153. package/dist/mcp-Bpx76OGU.mjs +945 -0
  154. package/dist/mcp-C1T7LI0H.mjs +1036 -0
  155. package/dist/mcp-C4Bjmf9m.mjs +1047 -0
  156. package/dist/mcp-CFxcdEcJ.mjs +733 -0
  157. package/dist/mcp-CwjThGi2.mjs +913 -0
  158. package/dist/mcp-D4OsB9lt.mjs +1047 -0
  159. package/dist/mcp-DJ2batvt.mjs +913 -0
  160. package/dist/mcp-D_LGvBTw.mjs +1047 -0
  161. package/dist/mcp-Ds_lPX_o.mjs +913 -0
  162. package/dist/plan-format-B6nbAw_c.mjs +132 -0
  163. package/dist/server-95UE5bgh.mjs +3367 -0
  164. package/dist/server-B0cS95Lf.mjs +3488 -0
  165. package/dist/server-Bj-6KlK5.mjs +3367 -0
  166. package/dist/server-Bpf84Il0.mjs +3367 -0
  167. package/dist/server-CYSpLbdA.mjs +3360 -0
  168. package/dist/server-ChT2DCaR.mjs +3701 -0
  169. package/dist/server-DJxysGxV.mjs +3701 -0
  170. package/dist/server-Dov7jbV3.mjs +3367 -0
  171. package/dist/server-eQ2E4Axo.mjs +3367 -0
  172. package/dist/server-lock-C_wPtEsH.mjs +909 -0
  173. package/dist/server-lock-Cbu3qHdu.mjs +924 -0
  174. package/dist/server-lock-D32inEn9.mjs +924 -0
  175. package/dist/server-lock-Ddcat0HF.mjs +851 -0
  176. package/dist/server-lock-Dq_1CJaF.mjs +1047 -0
  177. package/dist/server-lock-Ujbcgq28.mjs +954 -0
  178. package/dist/update-check-CXeSZANo.mjs +129 -0
  179. package/package.json +3 -2
  180. package/dist/client/assets/channel-CpWSvbvQ.js +0 -1
  181. package/dist/client/assets/chunk-QZHKN3VN-HhMIifwl.js +0 -1
  182. package/dist/client/assets/chunk-WU5MYG2G-CvIzXEms.js +0 -1
  183. package/dist/client/assets/classDiagram-4FO5ZUOK-DFu-dNVy.js +0 -1
  184. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-D7Xcgo6q.js +0 -1
  185. package/dist/client/assets/index-CMyunQt6.css +0 -1
  186. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D5igY81I.js +0 -1
@@ -0,0 +1,733 @@
1
+ import { o as parseDiffOptions, t as startServer } from "./server-CYSpLbdA.mjs";
2
+ import { a as writeServerLock, n as isLockAlive, o as formatComments, r as readServerLock, t as diffScopeKey } from "./server-lock-Ddcat0HF.mjs";
3
+ import { n as formatPlanReview } from "./plan-format-B6nbAw_c.mjs";
4
+ import { execFileSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, isAbsolute, resolve } from "node:path";
7
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+ //#region src/mcp.ts
12
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
13
+ function readPackageVersion() {
14
+ const packagePath = resolve(moduleDirectory, "..", "package.json");
15
+ const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
16
+ if (typeof pkg.version !== "string" || !pkg.version) throw new Error(`Invalid package version in ${packagePath}`);
17
+ return pkg.version;
18
+ }
19
+ const MCP_VERSION = readPackageVersion();
20
+ const READ_ONLY = {
21
+ readOnlyHint: true,
22
+ destructiveHint: false,
23
+ idempotentHint: true,
24
+ openWorldHint: false
25
+ };
26
+ const MUTATING = {
27
+ readOnlyHint: false,
28
+ destructiveHint: false,
29
+ idempotentHint: false,
30
+ openWorldHint: false
31
+ };
32
+ const IDEMPOTENT_MUTATION = {
33
+ ...MUTATING,
34
+ idempotentHint: true
35
+ };
36
+ const commentSchema = z.object({
37
+ id: z.string(),
38
+ filePath: z.string(),
39
+ side: z.enum(["deletions", "additions"]),
40
+ lineNumber: z.number(),
41
+ startLineNumber: z.number().optional(),
42
+ lineContent: z.string(),
43
+ body: z.string(),
44
+ status: z.enum(["open", "resolved"]),
45
+ createdAt: z.number(),
46
+ replies: z.array(z.object({
47
+ id: z.string(),
48
+ body: z.string(),
49
+ createdAt: z.number(),
50
+ role: z.enum(["user", "agent"]).optional(),
51
+ model: z.string().optional(),
52
+ createdAtPlanVersion: z.number().optional()
53
+ }))
54
+ });
55
+ function textResult(text, structuredContent) {
56
+ return {
57
+ content: [{
58
+ type: "text",
59
+ text
60
+ }],
61
+ structuredContent
62
+ };
63
+ }
64
+ /** Resolve and validate an immutable repository binding without invoking a shell. */
65
+ function resolveMcpRepository(repoPath = process.cwd(), explicit = false) {
66
+ if (explicit && !isAbsolute(repoPath)) throw new Error("diffing mcp: --repo must be an absolute path");
67
+ let candidate;
68
+ try {
69
+ candidate = realpathSync(repoPath);
70
+ if (!statSync(candidate).isDirectory()) throw new Error("not a directory");
71
+ } catch {
72
+ throw new Error(`diffing mcp: repository path is not an accessible directory: ${repoPath}`);
73
+ }
74
+ try {
75
+ return execFileSync("git", [
76
+ "-C",
77
+ candidate,
78
+ "rev-parse",
79
+ "--show-toplevel"
80
+ ], {
81
+ encoding: "utf-8",
82
+ stdio: [
83
+ "ignore",
84
+ "pipe",
85
+ "pipe"
86
+ ]
87
+ }).trim();
88
+ } catch {
89
+ throw new Error(`diffing mcp: path is not inside a Git repository: ${candidate}`);
90
+ }
91
+ }
92
+ function publicHost(host) {
93
+ return host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
94
+ }
95
+ function lockUrl(lock) {
96
+ if ((lock.mode ?? "web") === "tui" || lock.port <= 0) return null;
97
+ return `http://${publicHost(lock.host)}:${lock.port}`;
98
+ }
99
+ async function requestJson(base, path, init) {
100
+ let response;
101
+ try {
102
+ response = await fetch(`${base}${path}`, init);
103
+ } catch (error) {
104
+ if (error instanceof Error && error.name === "AbortError") throw new Error(`Request to ${path} was cancelled. Retry the MCP tool when ready.`);
105
+ const detail = error instanceof Error ? error.message : String(error);
106
+ throw new Error(`Cannot reach the diffing review server at ${base}: ${detail}. Call review_session_status, then start_review_session if needed.`);
107
+ }
108
+ const raw = await response.text();
109
+ if (!response.ok) {
110
+ let detail = raw.trim();
111
+ try {
112
+ const parsed = JSON.parse(raw);
113
+ if (typeof parsed.error === "string") detail = parsed.error;
114
+ } catch {}
115
+ throw new Error(`diffing server rejected ${init?.method ?? "GET"} ${path} with HTTP ${response.status}` + (detail ? `: ${detail}` : "") + ".");
116
+ }
117
+ try {
118
+ return JSON.parse(raw);
119
+ } catch {
120
+ throw new Error(`diffing server returned malformed JSON for ${path}. Check that client and server versions match.`);
121
+ }
122
+ }
123
+ function sameScope(lock, requestedScope, requestedArgs) {
124
+ if (lock.scope) return lock.scope === requestedScope;
125
+ return requestedArgs.length === 0;
126
+ }
127
+ function createMcpServer(options) {
128
+ const repoRoot = resolveMcpRepository(options.repoRoot, true);
129
+ const startServerFn = options.startServerFn ?? startServer;
130
+ const now = options.now ?? Date.now;
131
+ const readLock = options.readLock ?? readServerLock;
132
+ const writeLock = options.writeLock ?? writeServerLock;
133
+ const lockIsAlive = options.lockIsAlive ?? isLockAlive;
134
+ const defaultClientDir = existsSync(resolve(moduleDirectory, "client")) ? resolve(moduleDirectory, "client") : resolve(repoRoot, "dist/client");
135
+ const clientDir = options.clientDir ?? defaultClientDir;
136
+ 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.`;
137
+ const server = new McpServer({
138
+ name: "diffing",
139
+ version: MCP_VERSION
140
+ }, { instructions });
141
+ function liveLock() {
142
+ const lock = readLock(repoRoot);
143
+ return lock && lockIsAlive(lock, repoRoot) ? lock : null;
144
+ }
145
+ function requireBaseUrl() {
146
+ const lock = liveLock();
147
+ if (!lock) throw new Error(`No diffing web review session is running for ${repoRoot}. Call start_review_session, then retry this tool.`);
148
+ const url = lockUrl(lock);
149
+ if (!url) throw new Error("The active diffing session is TUI-only. Start a web review session before using MCP review tools.");
150
+ return url;
151
+ }
152
+ function sessionStatus() {
153
+ const lock = liveLock();
154
+ if (!lock) return {
155
+ repository: repoRoot,
156
+ serverState: "not-running",
157
+ mode: "none",
158
+ url: null,
159
+ managedBy: null,
160
+ diffArgs: [],
161
+ nextAction: "Call start_review_session, then get_diff."
162
+ };
163
+ const mode = lock.mode ?? "web";
164
+ return {
165
+ repository: repoRoot,
166
+ serverState: "running",
167
+ mode,
168
+ url: lockUrl(lock),
169
+ managedBy: lock.owner === "mcp" ? "mcp" : "user",
170
+ diffArgs: lock.diffArgs ?? [],
171
+ nextAction: 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."
172
+ };
173
+ }
174
+ server.registerTool("review_session_status", {
175
+ title: "Get review session status",
176
+ description: "Inspect the repository binding and active diffing session. Works when no web server is running; use its nextAction before other tools.",
177
+ inputSchema: {},
178
+ outputSchema: {
179
+ repository: z.string(),
180
+ serverState: z.enum(["running", "not-running"]),
181
+ mode: z.enum([
182
+ "none",
183
+ "web",
184
+ "gh-pr",
185
+ "tui"
186
+ ]),
187
+ url: z.string().nullable(),
188
+ managedBy: z.enum(["mcp", "user"]).nullable(),
189
+ diffArgs: z.array(z.string()),
190
+ nextAction: z.string()
191
+ },
192
+ annotations: READ_ONLY
193
+ }, async () => {
194
+ const status = sessionStatus();
195
+ 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 });
196
+ });
197
+ let startingSession = null;
198
+ server.registerTool("start_review_session", {
199
+ title: "Start or reuse a local review session",
200
+ description: "Idempotently reuse a matching web review session or start a headless loopback-only session on an OS-selected port. diffArgs are passed directly to diff parsing, never to a shell. A live incompatible user session is reported, never stopped.",
201
+ inputSchema: { diffArgs: z.array(z.string()).optional().describe("Optional git-diff arguments, for example [\"--staged\"] or [\"main...HEAD\", \"--\", \"src/\"].") },
202
+ outputSchema: {
203
+ status: z.enum(["started", "reused"]),
204
+ repository: z.string(),
205
+ url: z.string(),
206
+ mode: z.literal("web"),
207
+ managedBy: z.enum(["mcp", "user"]),
208
+ diffArgs: z.array(z.string()),
209
+ nextAction: z.string()
210
+ },
211
+ annotations: IDEMPOTENT_MUTATION
212
+ }, async ({ diffArgs = [] }) => {
213
+ if (startingSession) {
214
+ const result = await startingSession;
215
+ return textResult(`Review session ready at ${result.url}.`, result);
216
+ }
217
+ startingSession = (async () => {
218
+ const parsed = parseDiffOptions(diffArgs);
219
+ if (parsed.help || parsed.version || parsed.tui || diffArgs.includes("--terminal")) throw new Error("start_review_session accepts git-diff scope arguments, not CLI help/version/terminal/TUI flags.");
220
+ parsed.outputMode = "web";
221
+ parsed.host = "127.0.0.1";
222
+ parsed.port = void 0;
223
+ parsed.noOpen = true;
224
+ const requestedScope = diffScopeKey(parsed);
225
+ const existing = liveLock();
226
+ if (existing) {
227
+ const mode = existing.mode ?? "web";
228
+ 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.`);
229
+ 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.`);
230
+ const url = lockUrl(existing);
231
+ if (!url) throw new Error("The active diffing web session has no usable URL.");
232
+ return {
233
+ status: "reused",
234
+ repository: repoRoot,
235
+ url,
236
+ mode: "web",
237
+ managedBy: existing.owner === "mcp" ? "mcp" : "user",
238
+ diffArgs: existing.diffArgs ?? diffArgs,
239
+ nextAction: "Call get_diff to inspect the active diff."
240
+ };
241
+ }
242
+ const lock = {
243
+ port: (await startServerFn({
244
+ port: 0,
245
+ host: "127.0.0.1",
246
+ clientDir,
247
+ diffOpts: parsed
248
+ })).port,
249
+ host: "127.0.0.1",
250
+ pid: process.pid,
251
+ repoRoot,
252
+ startedAt: now(),
253
+ version: MCP_VERSION,
254
+ mode: "web",
255
+ scope: requestedScope,
256
+ diffArgs: [...diffArgs],
257
+ owner: "mcp"
258
+ };
259
+ writeLock(lock);
260
+ return {
261
+ status: "started",
262
+ repository: repoRoot,
263
+ url: lockUrl(lock),
264
+ mode: "web",
265
+ managedBy: "mcp",
266
+ diffArgs: [...diffArgs],
267
+ nextAction: "Call get_diff to inspect the active diff."
268
+ };
269
+ })();
270
+ try {
271
+ const result = await startingSession;
272
+ return textResult(`${result.status === "started" ? "Started" : "Reused"} review session at ${result.url}.`, result);
273
+ } finally {
274
+ startingSession = null;
275
+ }
276
+ });
277
+ server.registerTool("get_diff", {
278
+ title: "Get the active local diff",
279
+ description: "Fetch the complete patch and basic repository metadata from the active local web review. Start or locate the session first.",
280
+ inputSchema: {},
281
+ outputSchema: {
282
+ patch: z.string(),
283
+ repoName: z.string(),
284
+ branch: z.string(),
285
+ customMode: z.boolean(),
286
+ truncated: z.boolean().optional()
287
+ },
288
+ annotations: READ_ONLY
289
+ }, async () => {
290
+ const diff = await requestJson(requireBaseUrl(), "/api/diff");
291
+ const structured = {
292
+ patch: diff.patch,
293
+ repoName: diff.repoName,
294
+ branch: diff.branch,
295
+ customMode: diff.customMode,
296
+ ...typeof diff.truncated === "boolean" ? { truncated: diff.truncated } : {}
297
+ };
298
+ return textResult(diff.patch || "(The active diff is empty.)", structured);
299
+ });
300
+ server.registerTool("create_comment", {
301
+ title: "Create an inline review comment",
302
+ 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.",
303
+ inputSchema: {
304
+ filePath: z.string().min(1).describe("Repository-relative file path exactly as shown in the patch."),
305
+ side: z.enum(["deletions", "additions"]).describe("Which side of the patch contains the target line."),
306
+ lineNumber: z.number().int().positive().describe("Target line number on the selected side."),
307
+ startLineNumber: z.number().int().positive().optional().describe("Optional first line for a multiline comment."),
308
+ lineContent: z.string().describe("Target line text, used to preserve context in the review UI."),
309
+ body: z.string().min(1).describe("Actionable review comment in Markdown.")
310
+ },
311
+ outputSchema: {
312
+ status: z.literal("created"),
313
+ comment: commentSchema
314
+ },
315
+ annotations: MUTATING
316
+ }, async (input) => {
317
+ const comment = await requestJson(requireBaseUrl(), "/api/comments", {
318
+ method: "POST",
319
+ headers: { "Content-Type": "application/json" },
320
+ body: JSON.stringify(input)
321
+ });
322
+ return textResult(`Created inline comment ${comment.id} on ${comment.filePath}:${comment.lineNumber}.`, {
323
+ status: "created",
324
+ comment
325
+ });
326
+ });
327
+ server.registerTool("await_review", {
328
+ title: "Wait for code review handoff",
329
+ 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.",
330
+ inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe("Total wait budget in seconds; defaults to 570.") },
331
+ outputSchema: {
332
+ status: z.enum(["released", "timeout"]),
333
+ mode: z.enum(["standard", "comment-only"]),
334
+ round: z.number(),
335
+ openCount: z.number().optional(),
336
+ decision: z.enum([
337
+ "approved",
338
+ "changes-requested",
339
+ "rejected",
340
+ "comment-only"
341
+ ]).optional(),
342
+ comments: z.array(commentSchema).optional(),
343
+ nextAction: z.string()
344
+ },
345
+ annotations: READ_ONLY
346
+ }, async ({ timeoutSeconds }, extra) => {
347
+ const base = requireBaseUrl();
348
+ const budgetMs = (timeoutSeconds ?? 570) * 1e3;
349
+ const progressToken = extra?._meta?.progressToken;
350
+ let sinceRound = (await requestJson(base, "/api/review/status", { signal: extra?.signal })).round ?? 0;
351
+ const deadline = Date.now() + budgetMs;
352
+ let cycle = 0;
353
+ while (Date.now() < deadline) {
354
+ const remaining = Math.max(1, deadline - Date.now());
355
+ const result = await requestJson(base, `/api/review/await?timeoutMs=${Math.min(25e3, remaining)}&sinceRound=${sinceRound}`, { signal: extra?.signal });
356
+ if (result.status === "released") {
357
+ const payload = result.payload;
358
+ const structured = {
359
+ status: "released",
360
+ mode: payload.mode ?? "standard",
361
+ round: payload.round,
362
+ openCount: payload.openCount,
363
+ ...payload.decision ? { decision: payload.decision } : {},
364
+ comments: payload.comments,
365
+ 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."
366
+ };
367
+ return textResult(payload.commentXml, structured);
368
+ }
369
+ sinceRound = result.round ?? sinceRound;
370
+ cycle += 1;
371
+ if (progressToken !== void 0) await extra.sendNotification({
372
+ method: "notifications/progress",
373
+ params: {
374
+ progressToken,
375
+ progress: cycle,
376
+ total: Math.max(cycle, Math.ceil(budgetMs / 25e3)),
377
+ message: "Still waiting for a code-review handoff; long poll completed and will retry."
378
+ }
379
+ }).catch(() => {});
380
+ }
381
+ const structured = {
382
+ status: "timeout",
383
+ mode: "standard",
384
+ round: sinceRound,
385
+ nextAction: "No review was sent within the wait budget. Call await_review again to keep waiting."
386
+ };
387
+ return textResult(structured.nextAction, structured);
388
+ });
389
+ server.registerTool("list_comments", {
390
+ title: "List code review comments",
391
+ description: "Fetch code-review comments as XML plus structured data. Use openOnly=true when addressing the current review round.",
392
+ inputSchema: { openOnly: z.boolean().optional().describe("Return only unresolved comments when true.") },
393
+ outputSchema: { comments: z.array(commentSchema) },
394
+ annotations: READ_ONLY
395
+ }, async ({ openOnly }) => {
396
+ const all = await requestJson(requireBaseUrl(), "/api/comments");
397
+ const comments = openOnly ? all.filter((comment) => comment.status === "open") : all;
398
+ return textResult(formatComments(comments), { comments });
399
+ });
400
+ server.registerTool("reply_to_comment", {
401
+ title: "Reply to a code review comment",
402
+ description: "Post an agent reply to an existing code-review thread. Include concise evidence of the answer or applied change.",
403
+ inputSchema: {
404
+ commentId: z.string().min(1).describe("Comment id from list_comments or await_review."),
405
+ body: z.string().min(1).describe("Reply body in Markdown."),
406
+ model: z.string().optional().describe("Optional agent/model identifier shown in the UI.")
407
+ },
408
+ outputSchema: {
409
+ status: z.literal("replied"),
410
+ commentId: z.string()
411
+ },
412
+ annotations: MUTATING
413
+ }, async ({ commentId, body, model }) => {
414
+ await requestJson(requireBaseUrl(), `/api/comments/${encodeURIComponent(commentId)}/replies`, {
415
+ method: "POST",
416
+ headers: { "Content-Type": "application/json" },
417
+ body: JSON.stringify({
418
+ body,
419
+ role: "agent",
420
+ model
421
+ })
422
+ });
423
+ return textResult(`Replied to ${commentId}.`, {
424
+ status: "replied",
425
+ commentId
426
+ });
427
+ });
428
+ server.registerTool("resolve_comment", {
429
+ title: "Resolve a code review comment",
430
+ description: "Mark a code-review thread resolved after its request is fully addressed. Safe to retry.",
431
+ inputSchema: { commentId: z.string().min(1).describe("Comment id to resolve.") },
432
+ outputSchema: {
433
+ status: z.literal("resolved"),
434
+ commentId: z.string()
435
+ },
436
+ annotations: IDEMPOTENT_MUTATION
437
+ }, async ({ commentId }) => {
438
+ await requestJson(requireBaseUrl(), `/api/comments/${encodeURIComponent(commentId)}`, {
439
+ method: "PUT",
440
+ headers: { "Content-Type": "application/json" },
441
+ body: JSON.stringify({ status: "resolved" })
442
+ });
443
+ return textResult(`Resolved ${commentId}.`, {
444
+ status: "resolved",
445
+ commentId
446
+ });
447
+ });
448
+ server.registerTool("submit_plan", {
449
+ title: "Submit or resubmit a plan",
450
+ description: "Submit Markdown for human plan review. To revise after changes-requested, pass the same planId so a new version is created.",
451
+ inputSchema: {
452
+ title: z.string().optional().describe("Human-readable plan title."),
453
+ body: z.string().min(1).describe("Complete Markdown plan body."),
454
+ source: z.string().optional().describe("Optional source filename or workflow label."),
455
+ model: z.string().optional().describe("Optional agent/model identifier."),
456
+ planId: z.string().optional().describe("Existing plan id when resubmitting a revised version.")
457
+ },
458
+ outputSchema: {
459
+ status: z.literal("submitted"),
460
+ planId: z.string(),
461
+ version: z.number(),
462
+ url: z.string()
463
+ },
464
+ annotations: MUTATING
465
+ }, async ({ title, body, source, model, planId }) => {
466
+ const base = requireBaseUrl();
467
+ const plan = await requestJson(base, "/api/plans", {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify({
471
+ id: planId,
472
+ title,
473
+ body,
474
+ source,
475
+ model
476
+ })
477
+ });
478
+ const url = `${base}/plan/${plan.id}`;
479
+ return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. Call await_plan_review.`, {
480
+ status: "submitted",
481
+ planId: plan.id,
482
+ version: plan.version,
483
+ url
484
+ });
485
+ });
486
+ server.registerTool("await_plan_review", {
487
+ title: "Wait for plan review verdict",
488
+ 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.",
489
+ inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe("Total wait budget in seconds; defaults to 570.") },
490
+ outputSchema: {
491
+ status: z.enum(["released", "timeout"]),
492
+ mode: z.enum(["standard", "comment-only"]),
493
+ round: z.number(),
494
+ planId: z.string().optional(),
495
+ decision: z.enum([
496
+ "pending",
497
+ "approved",
498
+ "changes-requested",
499
+ "rejected"
500
+ ]).optional(),
501
+ decisionComment: z.string().optional(),
502
+ openCommentCount: z.number().optional(),
503
+ plan: z.unknown().optional(),
504
+ nextAction: z.string()
505
+ },
506
+ annotations: READ_ONLY
507
+ }, async ({ timeoutSeconds }, extra) => {
508
+ const base = requireBaseUrl();
509
+ const budgetMs = (timeoutSeconds ?? 570) * 1e3;
510
+ const progressToken = extra?._meta?.progressToken;
511
+ let sinceRound = (await requestJson(base, "/api/plan-review/status", { signal: extra?.signal })).round ?? 0;
512
+ const deadline = Date.now() + budgetMs;
513
+ let cycle = 0;
514
+ while (Date.now() < deadline) {
515
+ const remaining = Math.max(1, deadline - Date.now());
516
+ const result = await requestJson(base, `/api/plan-review/await?timeoutMs=${Math.min(25e3, remaining)}&sinceRound=${sinceRound}`, { signal: extra?.signal });
517
+ if (result.status === "released") {
518
+ const payload = result.payload;
519
+ 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.";
520
+ const structured = {
521
+ status: "released",
522
+ mode: payload.mode ?? "standard",
523
+ round: payload.round,
524
+ planId: payload.planId,
525
+ decision: payload.decision,
526
+ ...payload.decisionComment ? { decisionComment: payload.decisionComment } : {},
527
+ openCommentCount: payload.openCommentCount,
528
+ plan: payload.plan,
529
+ nextAction
530
+ };
531
+ return textResult(payload.reviewXml, structured);
532
+ }
533
+ sinceRound = result.round ?? sinceRound;
534
+ cycle += 1;
535
+ if (progressToken !== void 0) await extra.sendNotification({
536
+ method: "notifications/progress",
537
+ params: {
538
+ progressToken,
539
+ progress: cycle,
540
+ total: Math.max(cycle, Math.ceil(budgetMs / 25e3)),
541
+ message: "Still waiting for a plan verdict; long poll completed and will retry."
542
+ }
543
+ }).catch(() => {});
544
+ }
545
+ const structured = {
546
+ status: "timeout",
547
+ mode: "standard",
548
+ round: sinceRound,
549
+ nextAction: "No plan verdict arrived within the wait budget. Call await_plan_review again to keep waiting."
550
+ };
551
+ return textResult(structured.nextAction, structured);
552
+ });
553
+ server.registerTool("list_plans", {
554
+ title: "List submitted plans",
555
+ description: "List every submitted plan with its current verdict, version, and inline comments.",
556
+ inputSchema: {},
557
+ outputSchema: { plans: z.array(z.unknown()) },
558
+ annotations: READ_ONLY
559
+ }, async () => {
560
+ const plans = await requestJson(requireBaseUrl(), "/api/plans");
561
+ return textResult(plans.map((plan) => {
562
+ const open = (plan.comments ?? []).filter((comment) => comment.status === "open").length;
563
+ return `${plan.id} [${plan.decision}] v${plan.version} — ${open} open comment(s) — ${plan.title}`;
564
+ }).join("\n") || "No plans submitted yet.", { plans });
565
+ });
566
+ server.registerTool("get_plan", {
567
+ title: "Get a plan",
568
+ description: "Fetch one current plan as plan-review XML and structured data, including verdict and inline comments.",
569
+ inputSchema: { planId: z.string().min(1).describe("Plan id from submit_plan or list_plans.") },
570
+ outputSchema: { plan: z.unknown() },
571
+ annotations: READ_ONLY
572
+ }, async ({ planId }) => {
573
+ const plan = await requestJson(requireBaseUrl(), `/api/plans/${encodeURIComponent(planId)}`);
574
+ return textResult(formatPlanReview(plan), { plan });
575
+ });
576
+ server.registerTool("get_plan_versions", {
577
+ title: "List plan versions",
578
+ description: "List all submitted versions of a plan, oldest first.",
579
+ inputSchema: { planId: z.string().min(1).describe("Plan id to inspect.") },
580
+ outputSchema: { versions: z.array(z.unknown()) },
581
+ annotations: READ_ONLY
582
+ }, async ({ planId }) => {
583
+ const versions = await requestJson(requireBaseUrl(), `/api/plans/${encodeURIComponent(planId)}/versions`);
584
+ return textResult(versions.map((version) => {
585
+ const date = new Date(version.createdAt).toISOString().slice(0, 16).replace("T", " ");
586
+ return `v${version.version} — ${date} — ${version.title}`;
587
+ }).join("\n") || "No versions recorded.", { versions });
588
+ });
589
+ server.registerTool("get_plan_version", {
590
+ title: "Get a plan version",
591
+ description: "Fetch the current or a historical plan version as plan-review XML with version-anchored comments.",
592
+ inputSchema: {
593
+ planId: z.string().min(1).describe("Plan id to inspect."),
594
+ version: z.number().int().positive().optional().describe("Historical version number; omit for current.")
595
+ },
596
+ outputSchema: {
597
+ plan: z.unknown(),
598
+ version: z.unknown().optional(),
599
+ currentVersion: z.number().optional()
600
+ },
601
+ annotations: READ_ONLY
602
+ }, async ({ planId, version }) => {
603
+ const base = requireBaseUrl();
604
+ const encodedId = encodeURIComponent(planId);
605
+ const plan = await requestJson(base, `/api/plans/${encodedId}`);
606
+ if (version === void 0) return textResult(formatPlanReview(plan), { plan });
607
+ const data = await requestJson(base, `/api/plans/${encodedId}/versions/${version}`);
608
+ return textResult(formatPlanReview(plan, { viewingVersion: data.version.version }), {
609
+ plan,
610
+ version: data.version,
611
+ currentVersion: data.plan.currentVersion
612
+ });
613
+ });
614
+ async function findPlanForComment(base, commentId) {
615
+ return (await requestJson(base, "/api/plans")).find((plan) => (plan.comments ?? []).some((comment) => comment.id === commentId)) ?? null;
616
+ }
617
+ server.registerTool("reply_to_plan_comment", {
618
+ title: "Reply to a plan comment",
619
+ description: "Post an agent reply to an existing inline plan-review thread.",
620
+ inputSchema: {
621
+ commentId: z.string().min(1).describe("Plan comment id from get_plan or await_plan_review."),
622
+ body: z.string().min(1).describe("Reply body in Markdown."),
623
+ model: z.string().optional().describe("Optional agent/model identifier shown in the UI.")
624
+ },
625
+ outputSchema: {
626
+ status: z.literal("replied"),
627
+ commentId: z.string(),
628
+ planId: z.string()
629
+ },
630
+ annotations: MUTATING
631
+ }, async ({ commentId, body, model }) => {
632
+ const base = requireBaseUrl();
633
+ const plan = await findPlanForComment(base, commentId);
634
+ if (!plan) throw new Error(`Plan comment ${commentId} was not found. Refresh the plan before retrying.`);
635
+ await requestJson(base, `/api/plans/${encodeURIComponent(plan.id)}/comments/${encodeURIComponent(commentId)}/replies`, {
636
+ method: "POST",
637
+ headers: { "Content-Type": "application/json" },
638
+ body: JSON.stringify({
639
+ body,
640
+ role: "agent",
641
+ model
642
+ })
643
+ });
644
+ return textResult(`Replied to plan comment ${commentId}.`, {
645
+ status: "replied",
646
+ commentId,
647
+ planId: plan.id
648
+ });
649
+ });
650
+ server.registerTool("resolve_plan_comment", {
651
+ title: "Resolve a plan comment",
652
+ description: "Mark an inline plan-review thread resolved after it is fully addressed. Safe to retry.",
653
+ inputSchema: { commentId: z.string().min(1).describe("Plan comment id to resolve.") },
654
+ outputSchema: {
655
+ status: z.literal("resolved"),
656
+ commentId: z.string(),
657
+ planId: z.string()
658
+ },
659
+ annotations: IDEMPOTENT_MUTATION
660
+ }, async ({ commentId }) => {
661
+ const base = requireBaseUrl();
662
+ const plan = await findPlanForComment(base, commentId);
663
+ if (!plan) throw new Error(`Plan comment ${commentId} was not found. Refresh the plan before retrying.`);
664
+ await requestJson(base, `/api/plans/${encodeURIComponent(plan.id)}/comments/${encodeURIComponent(commentId)}`, {
665
+ method: "PUT",
666
+ headers: { "Content-Type": "application/json" },
667
+ body: JSON.stringify({ status: "resolved" })
668
+ });
669
+ return textResult(`Resolved plan comment ${commentId}.`, {
670
+ status: "resolved",
671
+ commentId,
672
+ planId: plan.id
673
+ });
674
+ });
675
+ server.registerResource("agent-guide", "diffing://agent-guide", {
676
+ title: "diffing agent guide",
677
+ description: "Portable quick reference for the native code-review and plan-review workflows.",
678
+ mimeType: "text/markdown"
679
+ }, async () => ({ contents: [{
680
+ uri: "diffing://agent-guide",
681
+ mimeType: "text/markdown",
682
+ text: `# diffing agent guide
683
+
684
+ This MCP connection is immutably bound to \`${repoRoot}\`.
685
+
686
+ ## Code review
687
+ 1. Call \`review_session_status\`; call \`start_review_session\` if needed.
688
+ 2. Call \`get_diff\` and inspect every changed file.
689
+ 3. Use \`create_comment\` for actionable inline findings.
690
+ 4. Use \`await_review\` for a human handoff; a timeout is safe to retry.
691
+ 5. In \`comment-only\` mode, reply without editing files.
692
+
693
+ ## Plan review
694
+ 1. Start or reuse a review session.
695
+ 2. Call \`submit_plan\`, then \`await_plan_review\`.
696
+ 3. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
697
+ 4. On \`approved\`, proceed; on \`rejected\`, stop.
698
+
699
+ All HTTP activity is loopback-only. MCP never terminates a user-owned session.`
700
+ }] }));
701
+ server.registerPrompt("review_local_changes", {
702
+ title: "Review local changes with diffing",
703
+ description: "Workflow prompt for inspecting a local diff and leaving inline review feedback.",
704
+ argsSchema: { focus: z.string().optional().describe("Optional review focus such as security, correctness, or tests.") }
705
+ }, async ({ focus }) => ({ messages: [{
706
+ role: "user",
707
+ content: {
708
+ type: "text",
709
+ 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}` : ""}.`
710
+ }
711
+ }] }));
712
+ server.registerPrompt("submit_plan_for_review", {
713
+ title: "Submit an implementation plan for review",
714
+ description: "Workflow prompt for submitting a plan and acting on the human verdict.",
715
+ argsSchema: { plan: z.string().describe("Complete Markdown implementation plan to submit.") }
716
+ }, async ({ plan }) => ({ messages: [{
717
+ role: "user",
718
+ content: {
719
+ type: "text",
720
+ 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.
721
+
722
+ ` + plan
723
+ }
724
+ }] }));
725
+ return server;
726
+ }
727
+ async function startMcpServer(options = {}) {
728
+ const repoRoot = resolveMcpRepository(options.repoPath ?? process.cwd(), options.repoPath !== void 0);
729
+ process.chdir(repoRoot);
730
+ await createMcpServer({ repoRoot }).connect(new StdioServerTransport());
731
+ }
732
+ //#endregion
733
+ export { startMcpServer };