@jonathanleelx/meddle 0.0.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 (361) hide show
  1. package/CONFIG_STRUCTURE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +298 -0
  4. package/bin/commands/mock/add.js +92 -0
  5. package/bin/commands/mock/delete.js +58 -0
  6. package/bin/commands/mock/index.js +50 -0
  7. package/bin/commands/mock/list.js +68 -0
  8. package/bin/commands/mock/toggle.js +64 -0
  9. package/bin/commands/mock/update.js +94 -0
  10. package/bin/commands/route/active.js +141 -0
  11. package/bin/commands/route/add.js +127 -0
  12. package/bin/commands/route/create.js +77 -0
  13. package/bin/commands/route/delete.js +120 -0
  14. package/bin/commands/route/index.js +59 -0
  15. package/bin/commands/route/list.js +67 -0
  16. package/bin/commands/route/preview.js +159 -0
  17. package/bin/commands/route/show.js +111 -0
  18. package/bin/commands/route/update.js +126 -0
  19. package/bin/commands/session/create.js +166 -0
  20. package/bin/commands/session/delete.js +69 -0
  21. package/bin/commands/session/index.js +61 -0
  22. package/bin/commands/session/list.js +42 -0
  23. package/bin/commands/session/prune.js +31 -0
  24. package/bin/commands/start.js +43 -0
  25. package/bin/commands/status.js +115 -0
  26. package/bin/commands/supervise.js +106 -0
  27. package/bin/commands/url.js +30 -0
  28. package/bin/doctor.js +356 -0
  29. package/bin/index +130 -0
  30. package/bin/lib/api-client.js +72 -0
  31. package/bin/lib/file-access.js +337 -0
  32. package/bin/lib/meddle-home.js +20 -0
  33. package/bin/lib/output.js +146 -0
  34. package/bin/lib/parsers.js +127 -0
  35. package/bin/lib/proxy-detect.js +105 -0
  36. package/bin/lib/route-preview.js +180 -0
  37. package/bin/lib/session-args.js +54 -0
  38. package/bin/lib/sessions.js +213 -0
  39. package/dist/cert.d.ts +10 -0
  40. package/dist/cert.d.ts.map +1 -0
  41. package/dist/cert.js +184 -0
  42. package/dist/cert.js.map +1 -0
  43. package/dist/core/application-identity.d.ts +55 -0
  44. package/dist/core/application-identity.d.ts.map +1 -0
  45. package/dist/core/application-identity.js +321 -0
  46. package/dist/core/application-identity.js.map +1 -0
  47. package/dist/core/body-utils.d.ts +2 -0
  48. package/dist/core/body-utils.d.ts.map +1 -0
  49. package/dist/core/body-utils.js +21 -0
  50. package/dist/core/body-utils.js.map +1 -0
  51. package/dist/core/browser.d.ts +2 -0
  52. package/dist/core/browser.d.ts.map +1 -0
  53. package/dist/core/browser.js +92 -0
  54. package/dist/core/browser.js.map +1 -0
  55. package/dist/core/client-identity.d.ts +27 -0
  56. package/dist/core/client-identity.d.ts.map +1 -0
  57. package/dist/core/client-identity.js +136 -0
  58. package/dist/core/client-identity.js.map +1 -0
  59. package/dist/core/config-diagnostics.d.ts +14 -0
  60. package/dist/core/config-diagnostics.d.ts.map +1 -0
  61. package/dist/core/config-diagnostics.js +137 -0
  62. package/dist/core/config-diagnostics.js.map +1 -0
  63. package/dist/core/connect-tunnel.d.ts +5 -0
  64. package/dist/core/connect-tunnel.d.ts.map +1 -0
  65. package/dist/core/connect-tunnel.js +45 -0
  66. package/dist/core/connect-tunnel.js.map +1 -0
  67. package/dist/core/content-encoding.d.ts +2 -0
  68. package/dist/core/content-encoding.d.ts.map +1 -0
  69. package/dist/core/content-encoding.js +77 -0
  70. package/dist/core/content-encoding.js.map +1 -0
  71. package/dist/core/custom-plugin-loader.d.ts +18 -0
  72. package/dist/core/custom-plugin-loader.d.ts.map +1 -0
  73. package/dist/core/custom-plugin-loader.js +178 -0
  74. package/dist/core/custom-plugin-loader.js.map +1 -0
  75. package/dist/core/h2-pool.d.ts +4 -0
  76. package/dist/core/h2-pool.d.ts.map +1 -0
  77. package/dist/core/h2-pool.js +180 -0
  78. package/dist/core/h2-pool.js.map +1 -0
  79. package/dist/core/inspection-dispatch.d.ts +5 -0
  80. package/dist/core/inspection-dispatch.d.ts.map +1 -0
  81. package/dist/core/inspection-dispatch.js +146 -0
  82. package/dist/core/inspection-dispatch.js.map +1 -0
  83. package/dist/core/log-rate-limit.d.ts +34 -0
  84. package/dist/core/log-rate-limit.d.ts.map +1 -0
  85. package/dist/core/log-rate-limit.js +52 -0
  86. package/dist/core/log-rate-limit.js.map +1 -0
  87. package/dist/core/map-local.d.ts +4 -0
  88. package/dist/core/map-local.d.ts.map +1 -0
  89. package/dist/core/map-local.js +195 -0
  90. package/dist/core/map-local.js.map +1 -0
  91. package/dist/core/meddle-home.d.ts +13 -0
  92. package/dist/core/meddle-home.d.ts.map +1 -0
  93. package/dist/core/meddle-home.js +56 -0
  94. package/dist/core/meddle-home.js.map +1 -0
  95. package/dist/core/mock-gate.d.ts +3 -0
  96. package/dist/core/mock-gate.d.ts.map +1 -0
  97. package/dist/core/mock-gate.js +20 -0
  98. package/dist/core/mock-gate.js.map +1 -0
  99. package/dist/core/mock-handler.d.ts +5 -0
  100. package/dist/core/mock-handler.d.ts.map +1 -0
  101. package/dist/core/mock-handler.js +339 -0
  102. package/dist/core/mock-handler.js.map +1 -0
  103. package/dist/core/on-mode-gate.d.ts +4 -0
  104. package/dist/core/on-mode-gate.d.ts.map +1 -0
  105. package/dist/core/on-mode-gate.js +64 -0
  106. package/dist/core/on-mode-gate.js.map +1 -0
  107. package/dist/core/pipeline-gate.d.ts +3 -0
  108. package/dist/core/pipeline-gate.d.ts.map +1 -0
  109. package/dist/core/pipeline-gate.js +34 -0
  110. package/dist/core/pipeline-gate.js.map +1 -0
  111. package/dist/core/pipeline.d.ts +6 -0
  112. package/dist/core/pipeline.d.ts.map +1 -0
  113. package/dist/core/pipeline.js +228 -0
  114. package/dist/core/pipeline.js.map +1 -0
  115. package/dist/core/plugin-bootstrap-runner.d.ts +6 -0
  116. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -0
  117. package/dist/core/plugin-bootstrap-runner.js +148 -0
  118. package/dist/core/plugin-bootstrap-runner.js.map +1 -0
  119. package/dist/core/plugin-bootstrap.d.ts +3 -0
  120. package/dist/core/plugin-bootstrap.d.ts.map +1 -0
  121. package/dist/core/plugin-bootstrap.js +15 -0
  122. package/dist/core/plugin-bootstrap.js.map +1 -0
  123. package/dist/core/plugin-compiler.d.ts +37 -0
  124. package/dist/core/plugin-compiler.d.ts.map +1 -0
  125. package/dist/core/plugin-compiler.js +172 -0
  126. package/dist/core/plugin-compiler.js.map +1 -0
  127. package/dist/core/plugin-context-factory.d.ts +29 -0
  128. package/dist/core/plugin-context-factory.d.ts.map +1 -0
  129. package/dist/core/plugin-context-factory.js +206 -0
  130. package/dist/core/plugin-context-factory.js.map +1 -0
  131. package/dist/core/plugin-generator.d.ts +47 -0
  132. package/dist/core/plugin-generator.d.ts.map +1 -0
  133. package/dist/core/plugin-generator.js +697 -0
  134. package/dist/core/plugin-generator.js.map +1 -0
  135. package/dist/core/plugin-health.d.ts +3 -0
  136. package/dist/core/plugin-health.d.ts.map +1 -0
  137. package/dist/core/plugin-health.js +54 -0
  138. package/dist/core/plugin-health.js.map +1 -0
  139. package/dist/core/plugin-intercept.d.ts +17 -0
  140. package/dist/core/plugin-intercept.d.ts.map +1 -0
  141. package/dist/core/plugin-intercept.js +133 -0
  142. package/dist/core/plugin-intercept.js.map +1 -0
  143. package/dist/core/plugin-runtime.d.ts +33 -0
  144. package/dist/core/plugin-runtime.d.ts.map +1 -0
  145. package/dist/core/plugin-runtime.js +254 -0
  146. package/dist/core/plugin-runtime.js.map +1 -0
  147. package/dist/core/proxy-context.d.ts +3 -0
  148. package/dist/core/proxy-context.d.ts.map +1 -0
  149. package/dist/core/proxy-context.js +129 -0
  150. package/dist/core/proxy-context.js.map +1 -0
  151. package/dist/core/proxy-record.d.ts +3 -0
  152. package/dist/core/proxy-record.d.ts.map +1 -0
  153. package/dist/core/proxy-record.js +32 -0
  154. package/dist/core/proxy-record.js.map +1 -0
  155. package/dist/core/refactor-config.d.ts +6 -0
  156. package/dist/core/refactor-config.d.ts.map +1 -0
  157. package/dist/core/refactor-config.js +45 -0
  158. package/dist/core/refactor-config.js.map +1 -0
  159. package/dist/core/refactor-status.d.ts +3 -0
  160. package/dist/core/refactor-status.d.ts.map +1 -0
  161. package/dist/core/refactor-status.js +19 -0
  162. package/dist/core/refactor-status.js.map +1 -0
  163. package/dist/core/remote-access.d.ts +40 -0
  164. package/dist/core/remote-access.d.ts.map +1 -0
  165. package/dist/core/remote-access.js +281 -0
  166. package/dist/core/remote-access.js.map +1 -0
  167. package/dist/core/route-decision.d.ts +3 -0
  168. package/dist/core/route-decision.d.ts.map +1 -0
  169. package/dist/core/route-decision.js +20 -0
  170. package/dist/core/route-decision.js.map +1 -0
  171. package/dist/core/route-loader.d.ts +14 -0
  172. package/dist/core/route-loader.d.ts.map +1 -0
  173. package/dist/core/route-loader.js +94 -0
  174. package/dist/core/route-loader.js.map +1 -0
  175. package/dist/core/route-preview.d.ts +15 -0
  176. package/dist/core/route-preview.d.ts.map +1 -0
  177. package/dist/core/route-preview.js +91 -0
  178. package/dist/core/route-preview.js.map +1 -0
  179. package/dist/core/runtime-health.d.ts +92 -0
  180. package/dist/core/runtime-health.d.ts.map +1 -0
  181. package/dist/core/runtime-health.js +205 -0
  182. package/dist/core/runtime-health.js.map +1 -0
  183. package/dist/core/sessions.d.ts +19 -0
  184. package/dist/core/sessions.d.ts.map +1 -0
  185. package/dist/core/sessions.js +93 -0
  186. package/dist/core/sessions.js.map +1 -0
  187. package/dist/core/shadow-compare.d.ts +8 -0
  188. package/dist/core/shadow-compare.d.ts.map +1 -0
  189. package/dist/core/shadow-compare.js +83 -0
  190. package/dist/core/shadow-compare.js.map +1 -0
  191. package/dist/core/shadow-readiness.d.ts +4 -0
  192. package/dist/core/shadow-readiness.d.ts.map +1 -0
  193. package/dist/core/shadow-readiness.js +118 -0
  194. package/dist/core/shadow-readiness.js.map +1 -0
  195. package/dist/core/short-response.d.ts +5 -0
  196. package/dist/core/short-response.d.ts.map +1 -0
  197. package/dist/core/short-response.js +18 -0
  198. package/dist/core/short-response.js.map +1 -0
  199. package/dist/core/static-server.d.ts +9 -0
  200. package/dist/core/static-server.d.ts.map +1 -0
  201. package/dist/core/static-server.js +136 -0
  202. package/dist/core/static-server.js.map +1 -0
  203. package/dist/core/types.d.ts +590 -0
  204. package/dist/core/types.d.ts.map +1 -0
  205. package/dist/core/types.js +4 -0
  206. package/dist/core/types.js.map +1 -0
  207. package/dist/helpers.d.ts +36 -0
  208. package/dist/helpers.d.ts.map +1 -0
  209. package/dist/helpers.js +290 -0
  210. package/dist/helpers.js.map +1 -0
  211. package/dist/plugins/builtin/index.d.ts +3 -0
  212. package/dist/plugins/builtin/index.d.ts.map +1 -0
  213. package/dist/plugins/builtin/index.js +23 -0
  214. package/dist/plugins/builtin/index.js.map +1 -0
  215. package/dist/plugins/builtin/logger-plugin.d.ts +8 -0
  216. package/dist/plugins/builtin/logger-plugin.d.ts.map +1 -0
  217. package/dist/plugins/builtin/logger-plugin.js +121 -0
  218. package/dist/plugins/builtin/logger-plugin.js.map +1 -0
  219. package/dist/plugins/builtin/mock-plugin.d.ts +3 -0
  220. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -0
  221. package/dist/plugins/builtin/mock-plugin.js +99 -0
  222. package/dist/plugins/builtin/mock-plugin.js.map +1 -0
  223. package/dist/plugins/builtin/router-plugin.d.ts +3 -0
  224. package/dist/plugins/builtin/router-plugin.d.ts.map +1 -0
  225. package/dist/plugins/builtin/router-plugin.js +36 -0
  226. package/dist/plugins/builtin/router-plugin.js.map +1 -0
  227. package/dist/server/agent/model.d.ts +30 -0
  228. package/dist/server/agent/model.d.ts.map +1 -0
  229. package/dist/server/agent/model.js +143 -0
  230. package/dist/server/agent/model.js.map +1 -0
  231. package/dist/server/agent/routes.d.ts +4 -0
  232. package/dist/server/agent/routes.d.ts.map +1 -0
  233. package/dist/server/agent/routes.js +188 -0
  234. package/dist/server/agent/routes.js.map +1 -0
  235. package/dist/server/agent/runtime.d.ts +12 -0
  236. package/dist/server/agent/runtime.d.ts.map +1 -0
  237. package/dist/server/agent/runtime.js +208 -0
  238. package/dist/server/agent/runtime.js.map +1 -0
  239. package/dist/server/agent/tools.d.ts +13 -0
  240. package/dist/server/agent/tools.d.ts.map +1 -0
  241. package/dist/server/agent/tools.js +1201 -0
  242. package/dist/server/agent/tools.js.map +1 -0
  243. package/dist/server/agent/types.d.ts +56 -0
  244. package/dist/server/agent/types.d.ts.map +1 -0
  245. package/dist/server/agent/types.js +3 -0
  246. package/dist/server/agent/types.js.map +1 -0
  247. package/dist/server/config.d.ts +9 -0
  248. package/dist/server/config.d.ts.map +1 -0
  249. package/dist/server/config.js +124 -0
  250. package/dist/server/config.js.map +1 -0
  251. package/dist/server/health.d.ts +4 -0
  252. package/dist/server/health.d.ts.map +1 -0
  253. package/dist/server/health.js +22 -0
  254. package/dist/server/health.js.map +1 -0
  255. package/dist/server/index.d.ts +239 -0
  256. package/dist/server/index.d.ts.map +1 -0
  257. package/dist/server/index.js +44 -0
  258. package/dist/server/index.js.map +1 -0
  259. package/dist/server/logs.d.ts +4 -0
  260. package/dist/server/logs.d.ts.map +1 -0
  261. package/dist/server/logs.js +40 -0
  262. package/dist/server/logs.js.map +1 -0
  263. package/dist/server/mocks.d.ts +9 -0
  264. package/dist/server/mocks.d.ts.map +1 -0
  265. package/dist/server/mocks.js +103 -0
  266. package/dist/server/mocks.js.map +1 -0
  267. package/dist/server/pipeline.d.ts +16 -0
  268. package/dist/server/pipeline.d.ts.map +1 -0
  269. package/dist/server/pipeline.js +159 -0
  270. package/dist/server/pipeline.js.map +1 -0
  271. package/dist/server/plugins.d.ts +32 -0
  272. package/dist/server/plugins.d.ts.map +1 -0
  273. package/dist/server/plugins.js +795 -0
  274. package/dist/server/plugins.js.map +1 -0
  275. package/dist/server/refactor.d.ts +4 -0
  276. package/dist/server/refactor.d.ts.map +1 -0
  277. package/dist/server/refactor.js +86 -0
  278. package/dist/server/refactor.js.map +1 -0
  279. package/dist/server/rule-files.d.ts +34 -0
  280. package/dist/server/rule-files.d.ts.map +1 -0
  281. package/dist/server/rule-files.js +389 -0
  282. package/dist/server/rule-files.js.map +1 -0
  283. package/dist/server/rules.d.ts +4 -0
  284. package/dist/server/rules.d.ts.map +1 -0
  285. package/dist/server/rules.js +40 -0
  286. package/dist/server/rules.js.map +1 -0
  287. package/dist/server/sessions.d.ts +4 -0
  288. package/dist/server/sessions.d.ts.map +1 -0
  289. package/dist/server/sessions.js +20 -0
  290. package/dist/server/sessions.js.map +1 -0
  291. package/index.js +950 -0
  292. package/mcp-server.js +794 -0
  293. package/package.json +92 -0
  294. package/plugins/builtin/index.ts +25 -0
  295. package/plugins/builtin/logger-plugin.ts +126 -0
  296. package/plugins/builtin/mock-plugin.ts +112 -0
  297. package/plugins/builtin/router-plugin.ts +35 -0
  298. package/web/README.md +111 -0
  299. package/web/dist/assets/ai-config-store-DQXVJVn_.js +1 -0
  300. package/web/dist/assets/alert-COF-DIAE.js +1 -0
  301. package/web/dist/assets/arrow-right-B4Mrlj2-.js +1 -0
  302. package/web/dist/assets/babel-DsprIpXZ.js +15 -0
  303. package/web/dist/assets/body-diff-view-D7dCWajG.js +6 -0
  304. package/web/dist/assets/button-BTToMaBW.js +1 -0
  305. package/web/dist/assets/checkbox-DIBarrJx.js +1 -0
  306. package/web/dist/assets/chevron-down-CPngAx4M.js +1 -0
  307. package/web/dist/assets/chevron-right-V1DGIDbL.js +1 -0
  308. package/web/dist/assets/circle-alert-Q8cZk9Rv.js +1 -0
  309. package/web/dist/assets/circle-x-C9USji_R.js +1 -0
  310. package/web/dist/assets/code-5_eJ_XpJ.js +1 -0
  311. package/web/dist/assets/code-fixer-fQtWZ5He.js +20 -0
  312. package/web/dist/assets/copy-BYjq1hdN.js +1 -0
  313. package/web/dist/assets/detail-panel-BX_hxeI0.js +3 -0
  314. package/web/dist/assets/dist-B8XUKgXe.js +1 -0
  315. package/web/dist/assets/dist-Bb9TeEub.js +1 -0
  316. package/web/dist/assets/dist-C5nMErq8.js +1 -0
  317. package/web/dist/assets/dist-C6eaYCUo.js +1 -0
  318. package/web/dist/assets/dist-Cn0y0IRn.js +1 -0
  319. package/web/dist/assets/dist-CxdY_A67.js +5 -0
  320. package/web/dist/assets/dist-D5Dj17P8.js +1 -0
  321. package/web/dist/assets/dist-DZJ9SMlJ.js +1 -0
  322. package/web/dist/assets/es2015-CfXu88Y7.js +41 -0
  323. package/web/dist/assets/estree-BP7k0NIt.js +44 -0
  324. package/web/dist/assets/file-system-access-CtcvBfj4.js +1 -0
  325. package/web/dist/assets/global-panel-shell-ChdO0dWd.js +6 -0
  326. package/web/dist/assets/health-panel-CQeCQ7gr.js +1 -0
  327. package/web/dist/assets/html-6FIWbqxX.js +25 -0
  328. package/web/dist/assets/index-CjqaTnE_.css +2 -0
  329. package/web/dist/assets/index-L7uWxTnp.js +18 -0
  330. package/web/dist/assets/mobile-proxy-panel-Dj1iLami.js +8 -0
  331. package/web/dist/assets/mock-config-BTTCw7RK.js +1 -0
  332. package/web/dist/assets/mock-editor-panel-C-aVfFme.js +1 -0
  333. package/web/dist/assets/monaco-editor-B3brLg8j.js +11 -0
  334. package/web/dist/assets/pencil-line-Cqe41E4h.js +1 -0
  335. package/web/dist/assets/plugin-code-editor-CPjCbJ9g.js +2 -0
  336. package/web/dist/assets/plugin-config-BzmCkvz3.js +1 -0
  337. package/web/dist/assets/plugin-generator-BnEafDF9.js +2 -0
  338. package/web/dist/assets/plugin-test-dialog-BWoWsWV4.js +7 -0
  339. package/web/dist/assets/plus-tVxSydJ8.js +1 -0
  340. package/web/dist/assets/postcss-Dnf30fQU.js +61 -0
  341. package/web/dist/assets/refresh-cw-CG3HaiQT.js +1 -0
  342. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +1 -0
  343. package/web/dist/assets/route-preview-BX7sfY0O.js +1 -0
  344. package/web/dist/assets/rule-ai-assistant-panel-WBiOOUw5.js +47 -0
  345. package/web/dist/assets/rule-config-CHpVij2M.css +1 -0
  346. package/web/dist/assets/rule-config-fTIZVwg1.js +15 -0
  347. package/web/dist/assets/select-CkFOB61l.js +1 -0
  348. package/web/dist/assets/server-Cpj4KNwN.js +1 -0
  349. package/web/dist/assets/settings-panel-COCYiIUg.js +1 -0
  350. package/web/dist/assets/shield-check-OqUmVLQt.js +1 -0
  351. package/web/dist/assets/smartphone-DKeDuJTU.js +1 -0
  352. package/web/dist/assets/standalone-CjxtLKw-.js +29 -0
  353. package/web/dist/assets/switch-C-Xk56TK.js +1 -0
  354. package/web/dist/assets/syntax-highlight-DVhPdkuV.js +5 -0
  355. package/web/dist/assets/table-CT97lAYq.js +1 -0
  356. package/web/dist/assets/textarea-C6NooVK1.js +1 -0
  357. package/web/dist/assets/wand-sparkles-wkul5vje.js +1 -0
  358. package/web/dist/assets/wifi-D9M35Ajj.js +1 -0
  359. package/web/dist/assets/zap-Djo4cgux.js +1 -0
  360. package/web/dist/index.html +24 -0
  361. package/web/dist/vite.svg +1 -0
package/mcp-server.js ADDED
@@ -0,0 +1,794 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for meddle
4
+ * 提供 start_proxy、路由规则管理、Mock 规则管理、Session 管理工具
5
+ */
6
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js')
7
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js')
8
+ // MCP SDK zod-compat 仅支持 zod/v3 或 zod/v4-mini 的内部结构,使用默认 zod 会报 _zod undefined
9
+ const z = require('zod/v3')
10
+ const path = require('path')
11
+ const fs = require('fs')
12
+ const { spawn } = require('child_process')
13
+ const { resolveMeddleHome } = require('./bin/lib/meddle-home')
14
+ const {
15
+ getSession, listSessions, createSession,
16
+ deleteSession, isPidAlive, allocatePort, generateId, sessionDir, readRegistry
17
+ } = require('./bin/lib/sessions')
18
+ const { GLOBAL_DEFAULT_PORT } = require('./bin/lib/session-args')
19
+
20
+ const meddleDir = resolveMeddleHome()
21
+ const mcpFile = path.join(meddleDir, 'mcp-proxy-url.json')
22
+ const DEFAULT_PROXY_BASE = 'http://127.0.0.1:9001'
23
+
24
+ let proxyProcess = null
25
+ let cachedProxyUrl = null
26
+
27
+ /** 获取代理 API 根地址(用于调用规则等接口) */
28
+ function getProxyBaseUrl() {
29
+ if (cachedProxyUrl) return cachedProxyUrl
30
+ try {
31
+ if (fs.existsSync(mcpFile)) {
32
+ const data = JSON.parse(fs.readFileSync(mcpFile, 'utf8'))
33
+ if (data.proxyUrl) return data.proxyUrl
34
+ }
35
+ } catch (_) {}
36
+ return DEFAULT_PROXY_BASE
37
+ }
38
+
39
+ /** 解析 session id 到目标代理 base URL。不传 session 时回退到默认代理。 */
40
+ function resolveSessionBaseUrl(sessionId) {
41
+ if (!sessionId) return getProxyBaseUrl()
42
+ const record = getSession(sessionId)
43
+ if (!record) throw new Error(`session not found: ${sessionId}`)
44
+ return `http://127.0.0.1:${record.port}`
45
+ }
46
+
47
+ /** 带 session 支持的 proxyApi。sessionId 可选,传了则转发到该 session 的 HTTP API */
48
+ function proxyApiForSession(sessionId, method, pathname, body) {
49
+ const base = resolveSessionBaseUrl(sessionId)
50
+ const url = base.replace(/\/$/, '') + pathname
51
+ const opts = { method, headers: { 'Content-Type': 'application/json' } }
52
+ if (body !== undefined) opts.body = typeof body === 'string' ? body : JSON.stringify(body)
53
+ return fetch(url, opts).then((res) => {
54
+ if (!res.ok) return res.text().then((t) => { throw new Error(t || res.statusText) })
55
+ return res.headers.get('content-type')?.includes('application/json') ? res.json() : res.text()
56
+ })
57
+ }
58
+
59
+ const FILE_PATTERN = /^file:\/\//
60
+ const LOCAL_FILE_PATTERN = /^[A-Za-z]:\\|^\/|^\\/
61
+
62
+ /** 解析规则文件内容为 pattern -> target 对象(与 helpers.parseEprcWithExclusions 一致) */
63
+ function parseEprcWithExclusions(content) {
64
+ const ruleMap = Object.create(null)
65
+ const excludeMap = Object.create(null)
66
+ content.split(/\r?\n/).forEach((line) => {
67
+ const trimmed = line.trim()
68
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//')) return
69
+ const parts = trimmed.split(/\s+/).filter(Boolean)
70
+ if (parts.length < 2) return
71
+
72
+ // Separate exclusions (tokens starting with !) from regular parts
73
+ const exclusions = []
74
+ const regularParts = parts.filter(p => {
75
+ if (p.startsWith('!')) {
76
+ exclusions.push(p.slice(1)) // Remove ! prefix
77
+ return false
78
+ }
79
+ return true
80
+ })
81
+
82
+ if (regularParts.length < 2) return // Need at least one rule and one target
83
+
84
+ let target = regularParts[regularParts.length - 1]
85
+ const rules = regularParts.slice(0, -1)
86
+ if (LOCAL_FILE_PATTERN.test(target) && !FILE_PATTERN.test(target)) {
87
+ target = 'file://' + target.replace(/\\/g, '/')
88
+ }
89
+ rules.forEach((rule) => {
90
+ const bm = rule.match(/\[([^\]]+)\]/)
91
+ let patternKey
92
+ if (bm) {
93
+ patternKey = rule.replace(bm[0], bm[1])
94
+ ruleMap[patternKey] = target + bm[0]
95
+ } else {
96
+ patternKey = rule
97
+ ruleMap[patternKey] = target
98
+ }
99
+ // Always set exclusions (even empty array) to override any previous rule
100
+ excludeMap[patternKey] = exclusions.slice() // Copy the array
101
+ })
102
+ })
103
+ return { ruleMap, excludeMap }
104
+ }
105
+
106
+ function parseEprc(content) {
107
+ return parseEprcWithExclusions(content).ruleMap
108
+ }
109
+
110
+ /** 将 pattern -> target 对象转回规则文件文本(与 helpers.ruleMapToEprcText 一致,保留 exclusions) */
111
+ function ruleMapToEprcText(ruleMap, excludeMap) {
112
+ const entries = Object.entries(ruleMap)
113
+ if (entries.length === 0) return ''
114
+ const byTargetAndExclusions = {}
115
+ entries.forEach(([rule, target]) => {
116
+ const bm = target.match(/\[([^\]]+)\]/)
117
+ const groupKey = bm ? target.replace(bm[0], '') : target
118
+ const displayRule = bm ? rule.replace(bm[1], bm[0]) : rule
119
+ const exclusions = excludeMap?.[rule] || []
120
+ const exclusionsKey = exclusions.join(',')
121
+ // Create a compound key that includes both target and exclusions
122
+ const compoundKey = `${groupKey}|||${exclusionsKey}`
123
+ if (!byTargetAndExclusions[compoundKey]) {
124
+ byTargetAndExclusions[compoundKey] = { target: groupKey, rules: [], exclusions }
125
+ }
126
+ byTargetAndExclusions[compoundKey].rules.push(displayRule)
127
+ })
128
+ return Object.entries(byTargetAndExclusions)
129
+ .map(([, { target, rules, exclusions }]) => {
130
+ let displayTarget = target
131
+ if (FILE_PATTERN.test(target)) {
132
+ displayTarget = target.replace(/^file:\/\//, '').replace(/\//g, path.sep)
133
+ }
134
+ const exclusionStr = exclusions.map(e => `!${e}`).join(' ')
135
+ const rulesStr = rules.join(' ')
136
+ return exclusionStr ? `${rulesStr} ${exclusionStr} ${displayTarget}` : `${rulesStr} ${displayTarget}`
137
+ })
138
+ .join('\n')
139
+ }
140
+
141
+ /** 请求代理 API */
142
+ function proxyApi(method, pathname, body) {
143
+ const base = getProxyBaseUrl()
144
+ const url = base.replace(/\/$/, '') + pathname
145
+ const opts = { method, headers: { 'Content-Type': 'application/json' } }
146
+ if (body !== undefined) opts.body = typeof body === 'string' ? body : JSON.stringify(body)
147
+ return fetch(url, opts).then((res) => {
148
+ const text = () => res.text()
149
+ if (!res.ok) return text().then((t) => { throw new Error(t || res.statusText) })
150
+ return res.headers.get('content-type')?.includes('application/json') ? res.json() : text()
151
+ })
152
+ }
153
+
154
+ function waitForProxyUrl(timeoutMs = 5000) {
155
+ return new Promise((resolve, reject) => {
156
+ const start = Date.now()
157
+ const interval = 50
158
+ const check = () => {
159
+ try {
160
+ if (fs.existsSync(mcpFile)) {
161
+ const data = JSON.parse(fs.readFileSync(mcpFile, 'utf8'))
162
+ if (data.proxyUrl) {
163
+ return resolve(data.proxyUrl)
164
+ }
165
+ }
166
+ } catch (_) {}
167
+ if (Date.now() - start > timeoutMs) {
168
+ return reject(new Error('等待代理启动超时'))
169
+ }
170
+ setTimeout(check, interval)
171
+ }
172
+ check()
173
+ })
174
+ }
175
+
176
+ /** 等待 session 的 HTTP API 就绪(轮询 /api/mocks) */
177
+ function waitForSessionReady(port, timeoutMs = 8000) {
178
+ return new Promise((resolve, reject) => {
179
+ const start = Date.now()
180
+ const check = () => {
181
+ fetch(`http://127.0.0.1:${port}/api/mocks`, { method: 'GET', signal: AbortSignal.timeout(500) })
182
+ .then((r) => { if (r.ok) resolve(); else retry() })
183
+ .catch(() => retry())
184
+ }
185
+ function retry() {
186
+ if (Date.now() - start > timeoutMs) {
187
+ return reject(new Error(`session did not become ready on port ${port} within ${timeoutMs}ms`))
188
+ }
189
+ setTimeout(check, 100)
190
+ }
191
+ check()
192
+ })
193
+ }
194
+
195
+ const mcpServer = new McpServer({
196
+ name: 'meddle',
197
+ version: '1.0.0'
198
+ })
199
+
200
+ mcpServer.registerTool('start_proxy', {
201
+ description: '启动 meddle 代理服务器,返回代理地址。',
202
+ inputSchema: {}
203
+ }, async () => {
204
+ if (proxyProcess && proxyProcess.exitCode === null) {
205
+ return {
206
+ content: [{ type: 'text', text: `代理已在运行: ${cachedProxyUrl}` }]
207
+ }
208
+ }
209
+ proxyProcess = null
210
+ cachedProxyUrl = null
211
+ const indexPath = path.join(__dirname, 'index.js')
212
+ const spawnEnv = { ...process.env, MEDDLE_MCP: '1', DEBUG: process.env.DEBUG || '' }
213
+ const child = spawn(process.execPath, [indexPath], {
214
+ env: spawnEnv,
215
+ stdio: ['ignore', 'pipe', 'pipe'],
216
+ cwd: process.cwd()
217
+ })
218
+ proxyProcess = child
219
+ child.stdout?.on('data', (d) => process.stderr.write(d))
220
+ child.stderr?.on('data', (d) => process.stderr.write(d))
221
+ child.on('error', (err) => {
222
+ proxyProcess = null
223
+ console.error('启动代理失败:', err)
224
+ })
225
+ child.on('exit', (code) => {
226
+ proxyProcess = null
227
+ cachedProxyUrl = null
228
+ })
229
+ try {
230
+ cachedProxyUrl = await waitForProxyUrl()
231
+ return {
232
+ content: [{ type: 'text', text: cachedProxyUrl }]
233
+ }
234
+ } catch (err) {
235
+ proxyProcess = null
236
+ child.kill()
237
+ return {
238
+ content: [{ type: 'text', text: `启动失败: ${err.message}` }],
239
+ isError: true
240
+ }
241
+ }
242
+ })
243
+
244
+ mcpServer.registerTool('get_proxy_url', {
245
+ description: '获取当前代理服务器 URL(用于配置系统代理或调用代理 API)。来源:本会话通过 start_proxy 启动的地址、~/.meddle/mcp-proxy-url.json,或默认 http://127.0.0.1:9001。',
246
+ inputSchema: {}
247
+ }, async () => {
248
+ const url = getProxyBaseUrl()
249
+ return { content: [{ type: 'text', text: url }] }
250
+ })
251
+
252
+ // ---------- Session 管理工具 ----------
253
+
254
+ mcpServer.registerTool('create_session', {
255
+ description: '创建一个新的隔离代理 session。每个 session 有独立的 MEDDLE_HOME(配置、路由、Mock)和端口,适用于多 Agent 或多项目需要隔离代理环境的场景。返回 session id、端口和代理地址。',
256
+ inputSchema: {
257
+ name: z.string().optional().describe('Session 标签名,用于生成易读的 session id(如 my-debug)。不传则用 session-timestamp'),
258
+ port: z.number().optional().describe('指定端口(9000-9999)。不传则自动分配可用端口')
259
+ }
260
+ }, async ({ name, port }) => {
261
+ // 1. id + port
262
+ const id = generateId(name)
263
+ let targetPort = port
264
+ if (targetPort) {
265
+ if (targetPort < 9000 || targetPort > 9999) {
266
+ return { content: [{ type: 'text', text: `--port 必须在 9000-9999 之间(传入 ${targetPort})` }], isError: true }
267
+ }
268
+ } else {
269
+ const registry = readRegistry()
270
+ targetPort = allocatePort(registry)
271
+ if (!targetPort) {
272
+ return { content: [{ type: 'text', text: '没有可用端口(9000-9999),请先清理孤儿 session' }], isError: true }
273
+ }
274
+ }
275
+
276
+ // 2. MEDDLE_HOME dir
277
+ const sDir = sessionDir(id)
278
+ fs.mkdirSync(sDir, { recursive: true })
279
+
280
+ // 3. spawn child
281
+ const indexPath = path.join(__dirname, 'index.js')
282
+ const childEnv = {
283
+ ...process.env,
284
+ MEDDLE_HOME: sDir,
285
+ PORT: String(targetPort),
286
+ MEDDLE_SESSION_ID: id,
287
+ MEDDLE_HEADLESS: '1',
288
+ MEDDLE_MCP: '1',
289
+ }
290
+ const child = spawn(process.execPath, [indexPath], {
291
+ env: childEnv,
292
+ stdio: ['ignore', 'ignore', 'ignore'],
293
+ cwd: process.cwd(),
294
+ detached: false,
295
+ })
296
+
297
+ if (!child.pid) {
298
+ return { content: [{ type: 'text', text: '子进程启动失败' }], isError: true }
299
+ }
300
+
301
+ // 4. wait for HTTP ready
302
+ try {
303
+ await waitForSessionReady(targetPort)
304
+ } catch (err) {
305
+ try { child.kill('SIGTERM') } catch (_) {}
306
+ return { content: [{ type: 'text', text: `session 启动超时: ${err.message}` }], isError: true }
307
+ }
308
+
309
+ // 5. register
310
+ try {
311
+ createSession({ id, label: name || '', port: targetPort, pid: child.pid, meddleHome: sDir })
312
+ } catch (err) {
313
+ try { child.kill('SIGTERM') } catch (_) {}
314
+ return { content: [{ type: 'text', text: `注册 session 失败: ${err.message}` }], isError: true }
315
+ }
316
+
317
+ const proxyUrl = `http://127.0.0.1:${targetPort}`
318
+ return {
319
+ content: [{ type: 'text', text: JSON.stringify({ id, port: targetPort, pid: child.pid, meddleHome: sDir, proxyUrl }, null, 2) }]
320
+ }
321
+ })
322
+
323
+ mcpServer.registerTool('delete_session', {
324
+ description: '删除指定 session。终止进程并从注册表移除。--clean 同时删除该 session 的数据目录。',
325
+ inputSchema: {
326
+ id: z.string().describe('要删除的 session id'),
327
+ clean: z.boolean().optional().describe('是否同时删除 session 数据目录')
328
+ }
329
+ }, async ({ id, clean }) => {
330
+ const record = getSession(id)
331
+ if (!record) {
332
+ return { content: [{ type: 'text', text: `session not found: ${id}` }], isError: true }
333
+ }
334
+
335
+ // 1. kill process if alive
336
+ let killed = false
337
+ if (isPidAlive(record.pid)) {
338
+ try {
339
+ process.kill(record.pid, 'SIGTERM')
340
+ killed = true
341
+ } catch (err) {
342
+ // ignore
343
+ }
344
+ }
345
+
346
+ // 2. remove from registry
347
+ deleteSession(id)
348
+
349
+ // 3. optionally clean data dir
350
+ let cleaned = false
351
+ if (clean) {
352
+ try {
353
+ if (fs.existsSync(record.meddleHome)) {
354
+ const sessionsRoot = path.dirname(record.meddleHome)
355
+ const expectedRoot = path.join(resolveMeddleHome(), 'sessions')
356
+ if (path.resolve(sessionsRoot) === path.resolve(expectedRoot)) {
357
+ fs.rmSync(record.meddleHome, { recursive: true, force: true })
358
+ cleaned = true
359
+ }
360
+ }
361
+ } catch (_) {}
362
+ }
363
+
364
+ return {
365
+ content: [{ type: 'text', text: JSON.stringify({ id, killed, cleaned, meddleHome: record.meddleHome }, null, 2) }]
366
+ }
367
+ })
368
+
369
+ mcpServer.registerTool('list_sessions', {
370
+ description: '列出所有已注册的代理 session,包含存活状态、端口、PID、创建时间等信息。',
371
+ inputSchema: {}
372
+ }, async () => {
373
+ const sessions = listSessions()
374
+ return {
375
+ content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }]
376
+ }
377
+ })
378
+
379
+ // ---------- Mock 规则(可选 session 参数) ----------
380
+
381
+ mcpServer.registerTool('mock_rule_list', {
382
+ description: '列出所有 Mock 规则。返回 id、name、urlPattern、method、statusCode、enabled 等。可选 session 参数指定操作的 session(不传则操作默认 session)。',
383
+ inputSchema: {
384
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session')
385
+ }
386
+ }, async ({ session }) => {
387
+ try {
388
+ const rules = await proxyApiForSession(session, 'GET', '/api/mocks')
389
+ return { content: [{ type: 'text', text: JSON.stringify(rules, null, 2) }] }
390
+ } catch (e) {
391
+ return { content: [{ type: 'text', text: e.message }], isError: true }
392
+ }
393
+ })
394
+
395
+ mcpServer.registerTool('mock_rule_add', {
396
+ description: '添加一条 Mock 规则。匹配到的请求将返回指定状态码、响应头和响应体。可选 session 参数指定操作的 session(不传则操作默认 session)。',
397
+ inputSchema: {
398
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
399
+ name: z.string().describe('规则名称,便于识别'),
400
+ urlPattern: z.string().describe('URL 匹配正则或字符串,如 example\\.com/api 或 .*\\.example\\.com'),
401
+ method: z.string().optional().describe('HTTP 方法,如 GET、POST、* 表示全部,默认 *'),
402
+ statusCode: z.number().optional().describe('响应状态码,默认 200'),
403
+ headers: z.record(z.string()).optional().describe('响应头,如 {"content-type":"application/json"}'),
404
+ body: z.string().optional().describe('响应体内容,默认空'),
405
+ bodyType: z.string().optional().describe('body 类型,如 inline,默认 inline'),
406
+ delay: z.number().optional().describe('延迟毫秒数,默认 0'),
407
+ enabled: z.boolean().optional().describe('是否启用,默认 true')
408
+ }
409
+ }, async (params) => {
410
+ try {
411
+ const { session, ...rest } = params
412
+ const body = {
413
+ name: rest.name ?? '',
414
+ urlPattern: rest.urlPattern ?? '',
415
+ method: rest.method ?? '*',
416
+ statusCode: rest.statusCode ?? 200,
417
+ delay: rest.delay ?? 0,
418
+ bodyType: rest.bodyType ?? 'inline',
419
+ headers: rest.headers ?? {},
420
+ body: rest.body ?? '',
421
+ enabled: rest.enabled !== false
422
+ }
423
+ const result = await proxyApiForSession(session, 'POST', '/api/mocks', body)
424
+ const rule = result.rule || result
425
+ return {
426
+ content: [{ type: 'text', text: `已添加 Mock 规则 id=${rule.id}: ${rule.name} (${rule.urlPattern})` }]
427
+ }
428
+ } catch (e) {
429
+ return { content: [{ type: 'text', text: e.message }], isError: true }
430
+ }
431
+ })
432
+
433
+ mcpServer.registerTool('mock_rule_update', {
434
+ description: '按 id 更新一条 Mock 规则,只传需要修改的字段。可选 session 参数指定操作的 session(不传则操作默认 session)。',
435
+ inputSchema: {
436
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
437
+ id: z.number().describe('规则 id,从 mock_rule_list 获取'),
438
+ name: z.string().optional(),
439
+ urlPattern: z.string().optional(),
440
+ method: z.string().optional(),
441
+ statusCode: z.number().optional(),
442
+ headers: z.record(z.string()).optional(),
443
+ body: z.string().optional(),
444
+ bodyType: z.string().optional(),
445
+ delay: z.number().optional(),
446
+ enabled: z.boolean().optional()
447
+ }
448
+ }, async (params) => {
449
+ try {
450
+ const { session, ...rest } = params
451
+ const id = rest.id
452
+ const updates = { ...rest }
453
+ delete updates.id
454
+ if (Object.keys(updates).length === 0) {
455
+ return { content: [{ type: 'text', text: '未提供要更新的字段' }], isError: true }
456
+ }
457
+ const result = await proxyApiForSession(session, 'PUT', `/api/mocks/${id}`, updates)
458
+ const rule = result.rule || result
459
+ return {
460
+ content: [{ type: 'text', text: `已更新 Mock 规则 id=${id}: ${rule.name ?? '(未改)'}` }]
461
+ }
462
+ } catch (e) {
463
+ return { content: [{ type: 'text', text: e.message }], isError: true }
464
+ }
465
+ })
466
+
467
+ mcpServer.registerTool('mock_rule_delete', {
468
+ description: '按 id 删除一条 Mock 规则。可选 session 参数指定操作的 session(不传则操作默认 session)。',
469
+ inputSchema: {
470
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
471
+ id: z.number().describe('规则 id,从 mock_rule_list 获取')
472
+ }
473
+ }, async ({ session, id }) => {
474
+ try {
475
+ await proxyApiForSession(session, 'DELETE', `/api/mocks/${id}`)
476
+ return { content: [{ type: 'text', text: `已删除 Mock 规则 id=${id}` }] }
477
+ } catch (e) {
478
+ return { content: [{ type: 'text', text: e.message }], isError: true }
479
+ }
480
+ })
481
+
482
+ // ---------- 路由规则(可选 session 参数) ----------
483
+
484
+ mcpServer.registerTool('route_rule_list', {
485
+ description: '列出所有路由规则文件;可选指定 ruleFile 获取该文件下的规则列表(pattern -> target 及 exclusions)。可选 session 参数指定操作的 session(不传则操作默认 session)。',
486
+ inputSchema: {
487
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
488
+ ruleFile: z.string().optional().describe('规则文件名(不含 .txt),不传则只返回文件列表')
489
+ }
490
+ }, async ({ session, ruleFile }) => {
491
+ try {
492
+ const files = await proxyApiForSession(session, 'GET', '/api/rule-files')
493
+ if (!ruleFile) {
494
+ return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] }
495
+ }
496
+ const name = encodeURIComponent(ruleFile.trim())
497
+ const content = await proxyApiForSession(session, 'GET', `/api/rule-files/${name}/content`)
498
+ const { ruleMap, excludeMap } = parseEprcWithExclusions(typeof content === 'string' ? content : String(content))
499
+ const displayRules = {}
500
+ for (const [pat, tgt] of Object.entries(ruleMap)) {
501
+ const bm = tgt.match(/\[([^\]]+)\]/)
502
+ const displayPat = bm ? pat.replace(bm[1], bm[0]) : pat
503
+ const displayTgt = bm ? tgt.replace(bm[0], '') : tgt
504
+ displayRules[displayPat] = {
505
+ target: displayTgt,
506
+ exclusions: excludeMap[pat] || []
507
+ }
508
+ }
509
+ let totalExclusions = 0
510
+ for (const exclusions of Object.values(excludeMap)) {
511
+ totalExclusions += exclusions.length
512
+ }
513
+ return {
514
+ content: [{ type: 'text', text: JSON.stringify({ ruleFile, rules: displayRules, ruleCount: Object.keys(displayRules).length, exclusionCount: totalExclusions }, null, 2) }]
515
+ }
516
+ } catch (e) {
517
+ return { content: [{ type: 'text', text: e.message }], isError: true }
518
+ }
519
+ })
520
+
521
+ mcpServer.registerTool('route_rule_active_get', {
522
+ description: '查看当前激活的路由规则文件。可选 session 参数指定操作的 session(不传则操作默认 session)。',
523
+ inputSchema: {
524
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session')
525
+ }
526
+ }, async ({ session }) => {
527
+ try {
528
+ const files = await proxyApiForSession(session, 'GET', '/api/rule-files')
529
+ const activeRuleFiles = Array.isArray(files) ? files.filter((item) => item && item.enabled) : []
530
+ const currentRuleFile = activeRuleFiles.length === 1 ? activeRuleFiles[0].name : null
531
+ return {
532
+ content: [{
533
+ type: 'text',
534
+ text: JSON.stringify({
535
+ currentRuleFile,
536
+ activeRuleFiles: activeRuleFiles.map((item) => item.name),
537
+ count: activeRuleFiles.length
538
+ }, null, 2)
539
+ }]
540
+ }
541
+ } catch (e) {
542
+ return { content: [{ type: 'text', text: e.message }], isError: true }
543
+ }
544
+ })
545
+
546
+ mcpServer.registerTool('route_rule_active_set', {
547
+ description: '设置当前激活的路由规则文件。可选 session 参数指定操作的 session(不传则操作默认 session)。',
548
+ inputSchema: {
549
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
550
+ ruleFile: z.string().describe('要设为当前激活的规则文件名(不含 .txt)')
551
+ }
552
+ }, async ({ session, ruleFile }) => {
553
+ try {
554
+ const targetName = ruleFile.trim()
555
+ const files = await proxyApiForSession(session, 'GET', '/api/rule-files')
556
+ if (!Array.isArray(files)) {
557
+ throw new Error('规则文件列表返回格式错误')
558
+ }
559
+ const targetFile = files.find((item) => item && item.name === targetName)
560
+ if (!targetFile) {
561
+ return { content: [{ type: 'text', text: `未找到规则文件: ${targetName}` }], isError: true }
562
+ }
563
+
564
+ for (const item of files) {
565
+ const nextEnabled = item.name === targetName
566
+ if (!!item.enabled === nextEnabled) continue
567
+ const name = encodeURIComponent(item.name)
568
+ await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}`, { enabled: nextEnabled })
569
+ }
570
+
571
+ return {
572
+ content: [{
573
+ type: 'text',
574
+ text: `已将当前激活路由规则设置为: ${targetName}`
575
+ }]
576
+ }
577
+ } catch (e) {
578
+ return { content: [{ type: 'text', text: e.message }], isError: true }
579
+ }
580
+ })
581
+
582
+ mcpServer.registerTool('route_rule_create_file', {
583
+ description: '创建新的路由规则文件。可选 session 参数指定操作的 session(不传则操作默认 session)。',
584
+ inputSchema: {
585
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
586
+ name: z.string().describe('规则文件名称(不含 .txt),如 dev、staging;非法字符会被替换为下划线'),
587
+ content: z.string().optional().describe('初始规则内容,每行一条规则。格式:「pattern1 pattern2 ... !exclusion1 !exclusion2 ... target」。规则:多个 pattern 共享同一 target;target 固定在最后;! 前缀表示 exclusion。pattern 匹配完整 URL,支持正则/通配符(如 *.wps.cn)/[marker]路径重写。target 若仅 host:port 则继承原请求协议/path/query。'),
588
+ enabled: z.boolean().optional().describe('是否加入当前启用的规则集,默认 true')
589
+ }
590
+ }, async ({ session, name, content = '', enabled = true }) => {
591
+ try {
592
+ const body = { name: name.trim(), content: content.trim(), enabled }
593
+ const result = await proxyApiForSession(session, 'POST', '/api/rule-files', body)
594
+ const rf = result.ruleFile || result
595
+ return {
596
+ content: [{
597
+ type: 'text',
598
+ text: `已创建规则文件: ${rf.name}(启用: ${rf.enabled},规则数: ${rf.ruleCount ?? 0})`
599
+ }]
600
+ }
601
+ } catch (e) {
602
+ return { content: [{ type: 'text', text: e.message }], isError: true }
603
+ }
604
+ })
605
+
606
+ mcpServer.registerTool('route_rule_add', {
607
+ description: '在指定规则文件中添加一条路由规则(pattern -> target)。若 pattern 已存在则覆盖。可选 session 参数指定操作的 session(不传则操作默认 session)。',
608
+ inputSchema: {
609
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
610
+ ruleFile: z.string().describe('规则文件名(不含 .txt)'),
611
+ pattern: z.string().describe('匹配完整请求 URL。支持:(1) 正则表达式,如 ^https://a\\.com/api; (2) 通配符,如 *.wps.cn 匹配 wps.cn、plus.wps.cn、deep.plus.wps.cn;(3) [marker] 路径重写,如 ^https://cdn.com/[assets] 将 assets 后的路径拼接到 target'),
612
+ target: z.string().describe('转发目标。若仅 host:port 则继承原请求的协议、pathname、query;若完整 URL 则直接使用;若原请求是 websocket 而 target 是 http(s),自动转为 ws(s)'),
613
+ exclusions: z.array(z.string()).optional().describe('排除规则列表,匹配完整 URL。若任一 exclusion 命中则跳过此规则继续匹配下一条。如 ["api/health", "^https://a\\.com/internal"]')
614
+ }
615
+ }, async ({ session, ruleFile, pattern, target, exclusions }) => {
616
+ try {
617
+ const name = encodeURIComponent(ruleFile.trim())
618
+ let content
619
+ try {
620
+ content = await proxyApiForSession(session, 'GET', `/api/rule-files/${name}/content`)
621
+ } catch (err) {
622
+ return { content: [{ type: 'text', text: `规则文件不存在或代理未启动: ${err.message}` }], isError: true }
623
+ }
624
+ const text = typeof content === 'string' ? content : String(content)
625
+ const { ruleMap, excludeMap } = parseEprcWithExclusions(text)
626
+ const pat = pattern.trim()
627
+ const tgt = target.trim()
628
+ const bm = pat.match(/\[([^\]]+)\]/)
629
+ if (bm) {
630
+ ruleMap[pat.replace(bm[0], bm[1])] = tgt + bm[0]
631
+ excludeMap[pat.replace(bm[0], bm[1])] = exclusions || []
632
+ } else {
633
+ ruleMap[pat] = tgt
634
+ excludeMap[pat] = exclusions || []
635
+ }
636
+ const newContent = ruleMapToEprcText(ruleMap, excludeMap)
637
+ await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}/content`, { content: newContent })
638
+ const exclusionInfo = exclusions && exclusions.length > 0 ? `,排除规则: ${exclusions.join(', ')}` : ''
639
+ return { content: [{ type: 'text', text: `已添加规则: ${pattern} -> ${target}${exclusionInfo}` }] }
640
+ } catch (e) {
641
+ return { content: [{ type: 'text', text: e.message }], isError: true }
642
+ }
643
+ })
644
+
645
+ mcpServer.registerTool('route_rule_update', {
646
+ description: '修改指定规则文件中某条规则的 target 和 exclusions(按 pattern 查找)。可选 session 参数指定操作的 session(不传则操作默认 session)。',
647
+ inputSchema: {
648
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
649
+ ruleFile: z.string().describe('规则文件名(不含 .txt)'),
650
+ pattern: z.string().describe('要修改的 pattern(需与现有规则一致,支持带 [marker] 的写法)'),
651
+ newTarget: z.string().optional().describe('新的转发目标。若仅 host:port 则继承原请求的协议、pathname、query;若完整 URL 则直接使用'),
652
+ exclusions: z.array(z.string()).optional().describe('新的排除规则列表,匹配完整 URL。设为空数组 [] 可清除所有排除规则')
653
+ }
654
+ }, async ({ session, ruleFile, pattern, newTarget, exclusions }) => {
655
+ try {
656
+ const name = encodeURIComponent(ruleFile.trim())
657
+ const pat = pattern.trim()
658
+ let content
659
+ try {
660
+ content = await proxyApiForSession(session, 'GET', `/api/rule-files/${name}/content`)
661
+ } catch (err) {
662
+ return { content: [{ type: 'text', text: `规则文件不存在或代理未启动: ${err.message}` }], isError: true }
663
+ }
664
+ const text = typeof content === 'string' ? content : String(content)
665
+ const { ruleMap, excludeMap } = parseEprcWithExclusions(text)
666
+ const bm = pat.match(/\[([^\]]+)\]/)
667
+ const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
668
+ if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
669
+ return { content: [{ type: 'text', text: `未找到 pattern: ${pat}` }], isError: true }
670
+ }
671
+ if (newTarget) {
672
+ const tgt = newTarget.trim()
673
+ ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
674
+ }
675
+ if (exclusions !== undefined) {
676
+ excludeMap[internalKey] = exclusions
677
+ }
678
+ const newContent = ruleMapToEprcText(ruleMap, excludeMap)
679
+ await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}/content`, { content: newContent })
680
+ const targetInfo = newTarget ? `目标: ${newTarget}` : ''
681
+ const exclusionInfo = exclusions !== undefined ? (exclusions.length > 0 ? `排除规则: ${exclusions.join(', ')}` : '已清除排除规则') : ''
682
+ const parts = [targetInfo, exclusionInfo].filter(Boolean).join(', ')
683
+ return { content: [{ type: 'text', text: `已更新规则: ${pat}${parts ? ` (${parts})` : ''}` }] }
684
+ } catch (e) {
685
+ return { content: [{ type: 'text', text: e.message }], isError: true }
686
+ }
687
+ })
688
+
689
+ mcpServer.registerTool('route_rule_delete', {
690
+ description: '从指定规则文件中删除一条路由规则(按 pattern)。可选 session 参数指定操作的 session(不传则操作默认 session)。',
691
+ inputSchema: {
692
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
693
+ ruleFile: z.string().describe('规则文件名(不含 .txt)'),
694
+ pattern: z.string().describe('要删除的 pattern(需与现有规则一致,支持带 [marker] 的写法)')
695
+ }
696
+ }, async ({ session, ruleFile, pattern }) => {
697
+ try {
698
+ const name = encodeURIComponent(ruleFile.trim())
699
+ const pat = pattern.trim()
700
+ let content
701
+ try {
702
+ content = await proxyApiForSession(session, 'GET', `/api/rule-files/${name}/content`)
703
+ } catch (err) {
704
+ return { content: [{ type: 'text', text: `规则文件不存在或代理未启动: ${err.message}` }], isError: true }
705
+ }
706
+ const text = typeof content === 'string' ? content : String(content)
707
+ const { ruleMap, excludeMap } = parseEprcWithExclusions(text)
708
+ const bm = pat.match(/\[([^\]]+)\]/)
709
+ const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
710
+ if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
711
+ return { content: [{ type: 'text', text: `未找到 pattern: ${pat}` }], isError: true }
712
+ }
713
+ delete ruleMap[internalKey]
714
+ delete excludeMap[internalKey]
715
+ const newContent = ruleMapToEprcText(ruleMap, excludeMap)
716
+ await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}/content`, { content: newContent })
717
+ return { content: [{ type: 'text', text: `已删除规则: ${pat}` }] }
718
+ } catch (e) {
719
+ return { content: [{ type: 'text', text: e.message }], isError: true }
720
+ }
721
+ })
722
+
723
+ mcpServer.registerTool('route_preview', {
724
+ description: '预览指定 URL 的路由转发目标。模拟路由匹配过程,返回命中的规则、目标地址等。可选 session 参数指定操作的 session(不传则操作默认 session)。',
725
+ inputSchema: {
726
+ session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
727
+ url: z.string().describe('待预览的完整 URL,如 https://api.example.com/v1/users'),
728
+ ruleFile: z.string().optional().describe('可选的规则文件名(不含 .txt),不传则使用所有已激活的规则文件'),
729
+ rulesText: z.string().optional().describe('可选的自定义规则文本(多行),用于测试临时规则;若提供则忽略 ruleFile 参数')
730
+ }
731
+ }, async ({ session, url, ruleFile, rulesText }) => {
732
+ try {
733
+ let actualRulesText = rulesText
734
+
735
+ if (!actualRulesText) {
736
+ if (ruleFile) {
737
+ const content = await proxyApiForSession(session, 'GET', `/api/rule-files/${encodeURIComponent(ruleFile.trim())}/content`)
738
+ actualRulesText = typeof content === 'string' ? content : String(content)
739
+ } else {
740
+ const files = await proxyApiForSession(session, 'GET', '/api/rule-files')
741
+ const activeFiles = Array.isArray(files) ? files.filter(item => item && item.enabled) : []
742
+
743
+ const contents = []
744
+ for (const file of activeFiles) {
745
+ const content = await proxyApiForSession(session, 'GET', `/api/rule-files/${encodeURIComponent(file.name)}/content`)
746
+ if (content) {
747
+ contents.push(typeof content === 'string' ? content : String(content))
748
+ }
749
+ }
750
+ actualRulesText = contents.join('\n')
751
+ }
752
+ }
753
+
754
+ if (!actualRulesText || !actualRulesText.trim()) {
755
+ return {
756
+ content: [{ type: 'text', text: '无可用规则:请先创建并激活路由规则文件,或提供 rulesText 参数' }],
757
+ isError: true
758
+ }
759
+ }
760
+
761
+ const result = await proxyApiForSession(session, 'POST', '/api/rules/preview', { url: url.trim(), rulesText: actualRulesText })
762
+
763
+ const output = {
764
+ inputUrl: result.inputUrl,
765
+ matched: result.matched,
766
+ resolvedUrl: result.resolvedUrl,
767
+ notes: result.notes
768
+ }
769
+
770
+ if (result.matched && result.matchedRule) {
771
+ output.matchedRule = {
772
+ pattern: result.matchedRule.pattern,
773
+ target: result.matchedRule.target,
774
+ kind: result.matchedRule.kind
775
+ }
776
+ }
777
+
778
+ return {
779
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
780
+ }
781
+ } catch (e) {
782
+ return { content: [{ type: 'text', text: e.message }], isError: true }
783
+ }
784
+ })
785
+
786
+ async function main() {
787
+ const transport = new StdioServerTransport()
788
+ await mcpServer.connect(transport)
789
+ }
790
+
791
+ main().catch((err) => {
792
+ console.error('MCP Server error:', err)
793
+ process.exit(1)
794
+ })