procsi 0.2.6

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 (296) hide show
  1. package/LICENSE +665 -0
  2. package/README.md +587 -0
  3. package/dist/cli/commands/clear.d.ts +3 -0
  4. package/dist/cli/commands/clear.d.ts.map +1 -0
  5. package/dist/cli/commands/clear.js +30 -0
  6. package/dist/cli/commands/clear.js.map +1 -0
  7. package/dist/cli/commands/daemon.d.ts +3 -0
  8. package/dist/cli/commands/daemon.d.ts.map +1 -0
  9. package/dist/cli/commands/daemon.js +59 -0
  10. package/dist/cli/commands/daemon.js.map +1 -0
  11. package/dist/cli/commands/debug-dump.d.ts +27 -0
  12. package/dist/cli/commands/debug-dump.d.ts.map +1 -0
  13. package/dist/cli/commands/debug-dump.js +102 -0
  14. package/dist/cli/commands/debug-dump.js.map +1 -0
  15. package/dist/cli/commands/helpers.d.ts +18 -0
  16. package/dist/cli/commands/helpers.d.ts.map +1 -0
  17. package/dist/cli/commands/helpers.js +34 -0
  18. package/dist/cli/commands/helpers.js.map +1 -0
  19. package/dist/cli/commands/init.d.ts +9 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -0
  21. package/dist/cli/commands/init.js +28 -0
  22. package/dist/cli/commands/init.js.map +1 -0
  23. package/dist/cli/commands/intercept.d.ts +9 -0
  24. package/dist/cli/commands/intercept.d.ts.map +1 -0
  25. package/dist/cli/commands/intercept.js +121 -0
  26. package/dist/cli/commands/intercept.js.map +1 -0
  27. package/dist/cli/commands/interceptors.d.ts +3 -0
  28. package/dist/cli/commands/interceptors.d.ts.map +1 -0
  29. package/dist/cli/commands/interceptors.js +163 -0
  30. package/dist/cli/commands/interceptors.js.map +1 -0
  31. package/dist/cli/commands/mcp.d.ts +3 -0
  32. package/dist/cli/commands/mcp.d.ts.map +1 -0
  33. package/dist/cli/commands/mcp.js +24 -0
  34. package/dist/cli/commands/mcp.js.map +1 -0
  35. package/dist/cli/commands/off.d.ts +8 -0
  36. package/dist/cli/commands/off.d.ts.map +1 -0
  37. package/dist/cli/commands/off.js +34 -0
  38. package/dist/cli/commands/off.js.map +1 -0
  39. package/dist/cli/commands/on.d.ts +9 -0
  40. package/dist/cli/commands/on.d.ts.map +1 -0
  41. package/dist/cli/commands/on.js +121 -0
  42. package/dist/cli/commands/on.js.map +1 -0
  43. package/dist/cli/commands/project.d.ts +3 -0
  44. package/dist/cli/commands/project.d.ts.map +1 -0
  45. package/dist/cli/commands/project.js +15 -0
  46. package/dist/cli/commands/project.js.map +1 -0
  47. package/dist/cli/commands/restart.d.ts +3 -0
  48. package/dist/cli/commands/restart.d.ts.map +1 -0
  49. package/dist/cli/commands/restart.js +35 -0
  50. package/dist/cli/commands/restart.js.map +1 -0
  51. package/dist/cli/commands/status.d.ts +3 -0
  52. package/dist/cli/commands/status.d.ts.map +1 -0
  53. package/dist/cli/commands/status.js +66 -0
  54. package/dist/cli/commands/status.js.map +1 -0
  55. package/dist/cli/commands/stop.d.ts +3 -0
  56. package/dist/cli/commands/stop.d.ts.map +1 -0
  57. package/dist/cli/commands/stop.js +24 -0
  58. package/dist/cli/commands/stop.js.map +1 -0
  59. package/dist/cli/commands/tui.d.ts +3 -0
  60. package/dist/cli/commands/tui.d.ts.map +1 -0
  61. package/dist/cli/commands/tui.js +36 -0
  62. package/dist/cli/commands/tui.js.map +1 -0
  63. package/dist/cli/commands/vars.d.ts +36 -0
  64. package/dist/cli/commands/vars.d.ts.map +1 -0
  65. package/dist/cli/commands/vars.js +207 -0
  66. package/dist/cli/commands/vars.js.map +1 -0
  67. package/dist/cli/index.d.ts +3 -0
  68. package/dist/cli/index.d.ts.map +1 -0
  69. package/dist/cli/index.js +37 -0
  70. package/dist/cli/index.js.map +1 -0
  71. package/dist/cli/tui/App.d.ts +15 -0
  72. package/dist/cli/tui/App.d.ts.map +1 -0
  73. package/dist/cli/tui/App.js +544 -0
  74. package/dist/cli/tui/App.js.map +1 -0
  75. package/dist/cli/tui/components/AccordionContent.d.ts +28 -0
  76. package/dist/cli/tui/components/AccordionContent.d.ts.map +1 -0
  77. package/dist/cli/tui/components/AccordionContent.js +87 -0
  78. package/dist/cli/tui/components/AccordionContent.js.map +1 -0
  79. package/dist/cli/tui/components/AccordionPanel.d.ts +38 -0
  80. package/dist/cli/tui/components/AccordionPanel.d.ts.map +1 -0
  81. package/dist/cli/tui/components/AccordionPanel.js +110 -0
  82. package/dist/cli/tui/components/AccordionPanel.js.map +1 -0
  83. package/dist/cli/tui/components/AccordionSection.d.ts +32 -0
  84. package/dist/cli/tui/components/AccordionSection.d.ts.map +1 -0
  85. package/dist/cli/tui/components/AccordionSection.js +41 -0
  86. package/dist/cli/tui/components/AccordionSection.js.map +1 -0
  87. package/dist/cli/tui/components/BodyView.d.ts +14 -0
  88. package/dist/cli/tui/components/BodyView.d.ts.map +1 -0
  89. package/dist/cli/tui/components/BodyView.js +39 -0
  90. package/dist/cli/tui/components/BodyView.js.map +1 -0
  91. package/dist/cli/tui/components/ExportModal.d.ts +34 -0
  92. package/dist/cli/tui/components/ExportModal.d.ts.map +1 -0
  93. package/dist/cli/tui/components/ExportModal.js +109 -0
  94. package/dist/cli/tui/components/ExportModal.js.map +1 -0
  95. package/dist/cli/tui/components/FilterBar.d.ts +21 -0
  96. package/dist/cli/tui/components/FilterBar.d.ts.map +1 -0
  97. package/dist/cli/tui/components/FilterBar.js +155 -0
  98. package/dist/cli/tui/components/FilterBar.js.map +1 -0
  99. package/dist/cli/tui/components/HeadersView.d.ts +13 -0
  100. package/dist/cli/tui/components/HeadersView.d.ts.map +1 -0
  101. package/dist/cli/tui/components/HeadersView.js +8 -0
  102. package/dist/cli/tui/components/HeadersView.js.map +1 -0
  103. package/dist/cli/tui/components/HelpModal.d.ts +13 -0
  104. package/dist/cli/tui/components/HelpModal.d.ts.map +1 -0
  105. package/dist/cli/tui/components/HelpModal.js +78 -0
  106. package/dist/cli/tui/components/HelpModal.js.map +1 -0
  107. package/dist/cli/tui/components/HintContent.d.ts +25 -0
  108. package/dist/cli/tui/components/HintContent.d.ts.map +1 -0
  109. package/dist/cli/tui/components/HintContent.js +44 -0
  110. package/dist/cli/tui/components/HintContent.js.map +1 -0
  111. package/dist/cli/tui/components/InfoModal.d.ts +15 -0
  112. package/dist/cli/tui/components/InfoModal.d.ts.map +1 -0
  113. package/dist/cli/tui/components/InfoModal.js +17 -0
  114. package/dist/cli/tui/components/InfoModal.js.map +1 -0
  115. package/dist/cli/tui/components/JsonExplorerModal.d.ts +24 -0
  116. package/dist/cli/tui/components/JsonExplorerModal.d.ts.map +1 -0
  117. package/dist/cli/tui/components/JsonExplorerModal.js +311 -0
  118. package/dist/cli/tui/components/JsonExplorerModal.js.map +1 -0
  119. package/dist/cli/tui/components/Modal.d.ts +26 -0
  120. package/dist/cli/tui/components/Modal.d.ts.map +1 -0
  121. package/dist/cli/tui/components/Modal.js +15 -0
  122. package/dist/cli/tui/components/Modal.js.map +1 -0
  123. package/dist/cli/tui/components/Panel.d.ts +19 -0
  124. package/dist/cli/tui/components/Panel.d.ts.map +1 -0
  125. package/dist/cli/tui/components/Panel.js +37 -0
  126. package/dist/cli/tui/components/Panel.js.map +1 -0
  127. package/dist/cli/tui/components/RequestDetails.d.ts +16 -0
  128. package/dist/cli/tui/components/RequestDetails.d.ts.map +1 -0
  129. package/dist/cli/tui/components/RequestDetails.js +23 -0
  130. package/dist/cli/tui/components/RequestDetails.js.map +1 -0
  131. package/dist/cli/tui/components/RequestList.d.ts +21 -0
  132. package/dist/cli/tui/components/RequestList.d.ts.map +1 -0
  133. package/dist/cli/tui/components/RequestList.js +30 -0
  134. package/dist/cli/tui/components/RequestList.js.map +1 -0
  135. package/dist/cli/tui/components/RequestListItem.d.ts +36 -0
  136. package/dist/cli/tui/components/RequestListItem.d.ts.map +1 -0
  137. package/dist/cli/tui/components/RequestListItem.js +130 -0
  138. package/dist/cli/tui/components/RequestListItem.js.map +1 -0
  139. package/dist/cli/tui/components/SaveModal.d.ts +30 -0
  140. package/dist/cli/tui/components/SaveModal.d.ts.map +1 -0
  141. package/dist/cli/tui/components/SaveModal.js +95 -0
  142. package/dist/cli/tui/components/SaveModal.js.map +1 -0
  143. package/dist/cli/tui/components/StatusBar.d.ts +39 -0
  144. package/dist/cli/tui/components/StatusBar.d.ts.map +1 -0
  145. package/dist/cli/tui/components/StatusBar.js +53 -0
  146. package/dist/cli/tui/components/StatusBar.js.map +1 -0
  147. package/dist/cli/tui/components/TextViewerModal.d.ts +19 -0
  148. package/dist/cli/tui/components/TextViewerModal.d.ts.map +1 -0
  149. package/dist/cli/tui/components/TextViewerModal.js +227 -0
  150. package/dist/cli/tui/components/TextViewerModal.js.map +1 -0
  151. package/dist/cli/tui/hooks/useBodyExport.d.ts +26 -0
  152. package/dist/cli/tui/hooks/useBodyExport.d.ts.map +1 -0
  153. package/dist/cli/tui/hooks/useBodyExport.js +173 -0
  154. package/dist/cli/tui/hooks/useBodyExport.js.map +1 -0
  155. package/dist/cli/tui/hooks/useExport.d.ts +29 -0
  156. package/dist/cli/tui/hooks/useExport.d.ts.map +1 -0
  157. package/dist/cli/tui/hooks/useExport.js +64 -0
  158. package/dist/cli/tui/hooks/useExport.js.map +1 -0
  159. package/dist/cli/tui/hooks/useRequests.d.ts +26 -0
  160. package/dist/cli/tui/hooks/useRequests.d.ts.map +1 -0
  161. package/dist/cli/tui/hooks/useRequests.js +131 -0
  162. package/dist/cli/tui/hooks/useRequests.js.map +1 -0
  163. package/dist/cli/tui/hooks/useSaveBinary.d.ts +26 -0
  164. package/dist/cli/tui/hooks/useSaveBinary.d.ts.map +1 -0
  165. package/dist/cli/tui/hooks/useSaveBinary.js +165 -0
  166. package/dist/cli/tui/hooks/useSaveBinary.js.map +1 -0
  167. package/dist/cli/tui/hooks/useSpinner.d.ts +5 -0
  168. package/dist/cli/tui/hooks/useSpinner.d.ts.map +1 -0
  169. package/dist/cli/tui/hooks/useSpinner.js +25 -0
  170. package/dist/cli/tui/hooks/useSpinner.js.map +1 -0
  171. package/dist/cli/tui/hooks/useStdoutDimensions.d.ts +11 -0
  172. package/dist/cli/tui/hooks/useStdoutDimensions.d.ts.map +1 -0
  173. package/dist/cli/tui/hooks/useStdoutDimensions.js +29 -0
  174. package/dist/cli/tui/hooks/useStdoutDimensions.js.map +1 -0
  175. package/dist/cli/tui/utils/binary.d.ts +24 -0
  176. package/dist/cli/tui/utils/binary.d.ts.map +1 -0
  177. package/dist/cli/tui/utils/binary.js +152 -0
  178. package/dist/cli/tui/utils/binary.js.map +1 -0
  179. package/dist/cli/tui/utils/clipboard.d.ts +9 -0
  180. package/dist/cli/tui/utils/clipboard.d.ts.map +1 -0
  181. package/dist/cli/tui/utils/clipboard.js +58 -0
  182. package/dist/cli/tui/utils/clipboard.js.map +1 -0
  183. package/dist/cli/tui/utils/content-type.d.ts +8 -0
  184. package/dist/cli/tui/utils/content-type.d.ts.map +1 -0
  185. package/dist/cli/tui/utils/content-type.js +10 -0
  186. package/dist/cli/tui/utils/content-type.js.map +1 -0
  187. package/dist/cli/tui/utils/curl.d.ts +9 -0
  188. package/dist/cli/tui/utils/curl.d.ts.map +1 -0
  189. package/dist/cli/tui/utils/curl.js +54 -0
  190. package/dist/cli/tui/utils/curl.js.map +1 -0
  191. package/dist/cli/tui/utils/filters.d.ts +6 -0
  192. package/dist/cli/tui/utils/filters.d.ts.map +1 -0
  193. package/dist/cli/tui/utils/filters.js +13 -0
  194. package/dist/cli/tui/utils/filters.js.map +1 -0
  195. package/dist/cli/tui/utils/formatters.d.ts +49 -0
  196. package/dist/cli/tui/utils/formatters.d.ts.map +1 -0
  197. package/dist/cli/tui/utils/formatters.js +200 -0
  198. package/dist/cli/tui/utils/formatters.js.map +1 -0
  199. package/dist/cli/tui/utils/har.d.ts +75 -0
  200. package/dist/cli/tui/utils/har.d.ts.map +1 -0
  201. package/dist/cli/tui/utils/har.js +117 -0
  202. package/dist/cli/tui/utils/har.js.map +1 -0
  203. package/dist/cli/tui/utils/json-tree.d.ts +69 -0
  204. package/dist/cli/tui/utils/json-tree.d.ts.map +1 -0
  205. package/dist/cli/tui/utils/json-tree.js +339 -0
  206. package/dist/cli/tui/utils/json-tree.js.map +1 -0
  207. package/dist/cli/tui/utils/open-external.d.ts +17 -0
  208. package/dist/cli/tui/utils/open-external.d.ts.map +1 -0
  209. package/dist/cli/tui/utils/open-external.js +57 -0
  210. package/dist/cli/tui/utils/open-external.js.map +1 -0
  211. package/dist/cli/tui/utils/syntax-highlight.d.ts +16 -0
  212. package/dist/cli/tui/utils/syntax-highlight.d.ts.map +1 -0
  213. package/dist/cli/tui/utils/syntax-highlight.js +64 -0
  214. package/dist/cli/tui/utils/syntax-highlight.js.map +1 -0
  215. package/dist/daemon/control.d.ts +21 -0
  216. package/dist/daemon/control.d.ts.map +1 -0
  217. package/dist/daemon/control.js +311 -0
  218. package/dist/daemon/control.js.map +1 -0
  219. package/dist/daemon/htpx-client.d.ts +8 -0
  220. package/dist/daemon/htpx-client.d.ts.map +1 -0
  221. package/dist/daemon/htpx-client.js +25 -0
  222. package/dist/daemon/htpx-client.js.map +1 -0
  223. package/dist/daemon/index.d.ts +3 -0
  224. package/dist/daemon/index.d.ts.map +1 -0
  225. package/dist/daemon/index.js +178 -0
  226. package/dist/daemon/index.js.map +1 -0
  227. package/dist/daemon/interceptor-loader.d.ts +30 -0
  228. package/dist/daemon/interceptor-loader.d.ts.map +1 -0
  229. package/dist/daemon/interceptor-loader.js +249 -0
  230. package/dist/daemon/interceptor-loader.js.map +1 -0
  231. package/dist/daemon/interceptor-runner.d.ts +39 -0
  232. package/dist/daemon/interceptor-runner.d.ts.map +1 -0
  233. package/dist/daemon/interceptor-runner.js +312 -0
  234. package/dist/daemon/interceptor-runner.js.map +1 -0
  235. package/dist/daemon/procsi-client.d.ts +8 -0
  236. package/dist/daemon/procsi-client.d.ts.map +1 -0
  237. package/dist/daemon/procsi-client.js +25 -0
  238. package/dist/daemon/procsi-client.js.map +1 -0
  239. package/dist/daemon/proxy.d.ts +34 -0
  240. package/dist/daemon/proxy.d.ts.map +1 -0
  241. package/dist/daemon/proxy.js +213 -0
  242. package/dist/daemon/proxy.js.map +1 -0
  243. package/dist/daemon/storage.d.ts +130 -0
  244. package/dist/daemon/storage.d.ts.map +1 -0
  245. package/dist/daemon/storage.js +761 -0
  246. package/dist/daemon/storage.js.map +1 -0
  247. package/dist/interceptors.d.ts +2 -0
  248. package/dist/interceptors.d.ts.map +1 -0
  249. package/dist/interceptors.js +2 -0
  250. package/dist/interceptors.js.map +1 -0
  251. package/dist/mcp/server.d.ts +110 -0
  252. package/dist/mcp/server.d.ts.map +1 -0
  253. package/dist/mcp/server.js +806 -0
  254. package/dist/mcp/server.js.map +1 -0
  255. package/dist/overrides/node.d.ts +30 -0
  256. package/dist/overrides/node.d.ts.map +1 -0
  257. package/dist/overrides/node.js +66 -0
  258. package/dist/overrides/node.js.map +1 -0
  259. package/dist/shared/config.d.ts +21 -0
  260. package/dist/shared/config.d.ts.map +1 -0
  261. package/dist/shared/config.js +83 -0
  262. package/dist/shared/config.js.map +1 -0
  263. package/dist/shared/content-type.d.ts +64 -0
  264. package/dist/shared/content-type.d.ts.map +1 -0
  265. package/dist/shared/content-type.js +145 -0
  266. package/dist/shared/content-type.js.map +1 -0
  267. package/dist/shared/control-client.d.ts +144 -0
  268. package/dist/shared/control-client.d.ts.map +1 -0
  269. package/dist/shared/control-client.js +272 -0
  270. package/dist/shared/control-client.js.map +1 -0
  271. package/dist/shared/daemon.d.ts +33 -0
  272. package/dist/shared/daemon.d.ts.map +1 -0
  273. package/dist/shared/daemon.js +231 -0
  274. package/dist/shared/daemon.js.map +1 -0
  275. package/dist/shared/logger.d.ts +47 -0
  276. package/dist/shared/logger.d.ts.map +1 -0
  277. package/dist/shared/logger.js +200 -0
  278. package/dist/shared/logger.js.map +1 -0
  279. package/dist/shared/project.d.ts +76 -0
  280. package/dist/shared/project.d.ts.map +1 -0
  281. package/dist/shared/project.js +185 -0
  282. package/dist/shared/project.js.map +1 -0
  283. package/dist/shared/proxy-info.d.ts +10 -0
  284. package/dist/shared/proxy-info.d.ts.map +1 -0
  285. package/dist/shared/proxy-info.js +15 -0
  286. package/dist/shared/proxy-info.js.map +1 -0
  287. package/dist/shared/types.d.ts +128 -0
  288. package/dist/shared/types.d.ts.map +1 -0
  289. package/dist/shared/types.js +5 -0
  290. package/dist/shared/types.js.map +1 -0
  291. package/dist/shared/version.d.ts +5 -0
  292. package/dist/shared/version.d.ts.map +1 -0
  293. package/dist/shared/version.js +21 -0
  294. package/dist/shared/version.js.map +1 -0
  295. package/package.json +113 -0
  296. package/skills/procsi/SKILL.md +228 -0
@@ -0,0 +1,806 @@
1
+ /**
2
+ * procsi MCP server — exposes read-only traffic inspection tools
3
+ * to MCP clients (AI agents, IDE integrations, etc.).
4
+ *
5
+ * Connects to the daemon's existing control socket to query captured traffic.
6
+ */
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+ import { ControlClient } from "../shared/control-client.js";
11
+ import { getProcsiPaths } from "../shared/project.js";
12
+ import { getProcsiVersion } from "../shared/version.js";
13
+ import { isTextContentType, isJsonContentType } from "../shared/content-type.js";
14
+ const DEFAULT_LIST_LIMIT = 50;
15
+ const MAX_LIST_LIMIT = 500;
16
+ const JSON_INDENT = 2;
17
+ function textResult(text, isError) {
18
+ return {
19
+ content: [{ type: "text", text }],
20
+ ...(isError ? { isError: true } : {}),
21
+ };
22
+ }
23
+ const FORMAT_SCHEMA = z
24
+ .enum(["text", "json"])
25
+ .optional()
26
+ .default("text")
27
+ .describe('Output format: "text" (default) or "json" (structured).');
28
+ function jsonResult(data) {
29
+ return {
30
+ content: [{ type: "text", text: JSON.stringify(data, null, JSON_INDENT) }],
31
+ };
32
+ }
33
+ /**
34
+ * Format a body buffer for display in text output.
35
+ *
36
+ * - **Binary** (non-text content type): returns `[binary data, N bytes]`
37
+ * - **JSON** (JSON content type): pretty-prints inside a ```json code fence. Falls back to raw text on parse failure.
38
+ * - **Other text / unknown**: raw UTF-8 string
39
+ */
40
+ export function formatBody(body, contentType) {
41
+ // Binary detection — if we know it's not text, show a placeholder
42
+ if (contentType && !isTextContentType(contentType)) {
43
+ const size = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(String(body));
44
+ return `[binary data, ${formatSize(size)}]`;
45
+ }
46
+ const text = bufferToString(body);
47
+ // JSON pretty-printing
48
+ if (isJsonContentType(contentType)) {
49
+ try {
50
+ const parsed = JSON.parse(text);
51
+ return "```json\n" + JSON.stringify(parsed, null, JSON_INDENT) + "\n```";
52
+ }
53
+ catch {
54
+ // Malformed JSON — fall through to raw text
55
+ }
56
+ }
57
+ return text;
58
+ }
59
+ /**
60
+ * Convert a CapturedRequest into a JSON-serialisable object.
61
+ * Text bodies become UTF-8 strings; binary bodies become null with a binary flag.
62
+ */
63
+ export function serialiseRequest(req) {
64
+ const reqContentType = req.requestHeaders?.["content-type"];
65
+ const resContentType = req.responseHeaders?.["content-type"];
66
+ const reqBodyBinary = reqContentType ? !isTextContentType(reqContentType) : false;
67
+ const resBodyBinary = resContentType ? !isTextContentType(resContentType) : false;
68
+ return {
69
+ id: req.id,
70
+ sessionId: req.sessionId,
71
+ ...(req.label !== undefined ? { label: req.label } : {}),
72
+ timestamp: new Date(req.timestamp).toISOString(),
73
+ method: req.method,
74
+ url: req.url,
75
+ host: req.host,
76
+ path: req.path,
77
+ requestHeaders: req.requestHeaders,
78
+ requestBody: req.requestBody != null ? (reqBodyBinary ? null : bufferToString(req.requestBody)) : null,
79
+ requestBodyTruncated: req.requestBodyTruncated ?? false,
80
+ requestBodyBinary: reqBodyBinary,
81
+ ...(req.responseStatus !== undefined ? { responseStatus: req.responseStatus } : {}),
82
+ ...(req.responseHeaders ? { responseHeaders: req.responseHeaders } : {}),
83
+ responseBody: req.responseBody != null ? (resBodyBinary ? null : bufferToString(req.responseBody)) : null,
84
+ responseBodyTruncated: req.responseBodyTruncated ?? false,
85
+ responseBodyBinary: resBodyBinary,
86
+ ...(req.durationMs !== undefined ? { durationMs: req.durationMs } : {}),
87
+ ...(req.interceptedBy !== undefined ? { interceptedBy: req.interceptedBy } : {}),
88
+ ...(req.interceptionType !== undefined ? { interceptionType: req.interceptionType } : {}),
89
+ };
90
+ }
91
+ /**
92
+ * Serialise a CapturedRequest into a human/AI-readable text block.
93
+ * Buffers are converted to UTF-8 strings where possible.
94
+ */
95
+ export function formatRequest(req) {
96
+ const lines = [];
97
+ lines.push(`## ${req.method} ${req.url}`);
98
+ lines.push(`**ID:** ${req.id}`);
99
+ lines.push(`**Timestamp:** ${new Date(req.timestamp).toISOString()}`);
100
+ lines.push(`**Host:** ${req.host}`);
101
+ lines.push(`**Path:** ${req.path}`);
102
+ // Surface content-types for quick scanning
103
+ const reqContentType = req.requestHeaders?.["content-type"];
104
+ const resContentType = req.responseHeaders?.["content-type"];
105
+ if (reqContentType) {
106
+ lines.push(`**Request Content-Type:** ${reqContentType}`);
107
+ }
108
+ if (resContentType) {
109
+ lines.push(`**Response Content-Type:** ${resContentType}`);
110
+ }
111
+ if (req.responseStatus !== undefined) {
112
+ lines.push(`**Status:** ${req.responseStatus}`);
113
+ }
114
+ if (req.durationMs !== undefined) {
115
+ lines.push(`**Duration:** ${req.durationMs}ms`);
116
+ }
117
+ if (req.interceptedBy) {
118
+ const type = req.interceptionType ?? "modified";
119
+ lines.push(`**Intercepted by:** ${req.interceptedBy} (${type})`);
120
+ }
121
+ // Request headers
122
+ if (req.requestHeaders && Object.keys(req.requestHeaders).length > 0) {
123
+ lines.push("");
124
+ lines.push("### Request Headers");
125
+ for (const [key, value] of Object.entries(req.requestHeaders)) {
126
+ lines.push(`- **${key}:** ${value}`);
127
+ }
128
+ }
129
+ // Request body
130
+ if (req.requestBody) {
131
+ lines.push("");
132
+ lines.push("### Request Body");
133
+ lines.push(formatBody(req.requestBody, reqContentType));
134
+ if (req.requestBodyTruncated) {
135
+ lines.push("_(truncated)_");
136
+ }
137
+ }
138
+ // Response headers
139
+ if (req.responseHeaders && Object.keys(req.responseHeaders).length > 0) {
140
+ lines.push("");
141
+ lines.push("### Response Headers");
142
+ for (const [key, value] of Object.entries(req.responseHeaders)) {
143
+ lines.push(`- **${key}:** ${value}`);
144
+ }
145
+ }
146
+ // Response body
147
+ if (req.responseBody) {
148
+ lines.push("");
149
+ lines.push("### Response Body");
150
+ lines.push(formatBody(req.responseBody, resContentType));
151
+ if (req.responseBodyTruncated) {
152
+ lines.push("_(truncated)_");
153
+ }
154
+ }
155
+ return lines.join("\n");
156
+ }
157
+ /**
158
+ * Convert a Buffer (or serialised Buffer from JSON) to a UTF-8 string.
159
+ */
160
+ function bufferToString(buf) {
161
+ if (Buffer.isBuffer(buf)) {
162
+ return buf.toString("utf-8");
163
+ }
164
+ // Handle serialised Buffer from JSON (already revived by ControlClient)
165
+ if (typeof buf === "string") {
166
+ return buf;
167
+ }
168
+ return String(buf);
169
+ }
170
+ const BYTES_PER_KB = 1024;
171
+ /**
172
+ * Format a byte count into a compact human-readable string.
173
+ * Returns "0B" for zero, otherwise e.g. "500B", "1.2KB", "3.5MB".
174
+ */
175
+ export function formatSize(bytes) {
176
+ if (bytes === 0)
177
+ return "0B";
178
+ if (bytes < BYTES_PER_KB)
179
+ return `${bytes}B`;
180
+ const kb = bytes / BYTES_PER_KB;
181
+ if (kb < BYTES_PER_KB)
182
+ return `${kb.toFixed(1)}KB`;
183
+ const mb = kb / BYTES_PER_KB;
184
+ return `${mb.toFixed(1)}MB`;
185
+ }
186
+ /**
187
+ * Format a Session into a concise one-line description.
188
+ */
189
+ export function formatSession(session) {
190
+ const label = session.label ? ` (${session.label})` : "";
191
+ const started = new Date(session.startedAt).toISOString();
192
+ return `[${session.id}] PID ${session.pid}${label} — started ${started}`;
193
+ }
194
+ /**
195
+ * Format an InterceptorInfo into a concise one-line description.
196
+ */
197
+ export function formatInterceptor(info) {
198
+ const matchLabel = info.hasMatch ? "[has match]" : "[match all]";
199
+ const errorSuffix = info.error ? ` \u2014 Error: ${info.error}` : "";
200
+ return `${info.name} (${info.sourceFile}) ${matchLabel}${errorSuffix}`;
201
+ }
202
+ /**
203
+ * Format a summary into a concise one-line description.
204
+ */
205
+ export function formatSummary(req) {
206
+ const ts = new Date(req.timestamp).toISOString();
207
+ const status = req.responseStatus !== undefined ? ` → ${req.responseStatus}` : " → pending";
208
+ const duration = req.durationMs !== undefined ? ` (${req.durationMs}ms)` : "";
209
+ const hasBody = req.requestBodySize > 0 || req.responseBodySize > 0;
210
+ const bodySizes = hasBody
211
+ ? ` [^${formatSize(req.requestBodySize)} v${formatSize(req.responseBodySize)}]`
212
+ : "";
213
+ const interceptionTag = req.interceptionType === "mocked" ? " [M]" : req.interceptionType === "modified" ? " [I]" : "";
214
+ return `[${req.id}] ${ts} ${req.method} ${req.url}${status}${duration}${bodySizes}${interceptionTag}`;
215
+ }
216
+ const MIN_HTTP_STATUS = 100;
217
+ const MAX_HTTP_STATUS = 599;
218
+ /**
219
+ * Validate and normalise a status_range value.
220
+ * Accepts Nxx patterns, exact codes (e.g. "401"), and numeric ranges (e.g. "500-503").
221
+ */
222
+ function validateStatusRange(value) {
223
+ // Nxx pattern
224
+ if (/^[1-5]xx$/.test(value)) {
225
+ return value;
226
+ }
227
+ // Numeric range — e.g. "500-503"
228
+ const rangeMatch = value.match(/^(\d{3})-(\d{3})$/);
229
+ if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
230
+ const low = parseInt(rangeMatch[1], 10);
231
+ const high = parseInt(rangeMatch[2], 10);
232
+ if (low < MIN_HTTP_STATUS || low > MAX_HTTP_STATUS) {
233
+ throw new Error(`Invalid status_range "${value}". Lower bound ${low} is outside valid HTTP status range (${MIN_HTTP_STATUS}-${MAX_HTTP_STATUS}).`);
234
+ }
235
+ if (high < MIN_HTTP_STATUS || high > MAX_HTTP_STATUS) {
236
+ throw new Error(`Invalid status_range "${value}". Upper bound ${high} is outside valid HTTP status range (${MIN_HTTP_STATUS}-${MAX_HTTP_STATUS}).`);
237
+ }
238
+ if (low > high) {
239
+ throw new Error(`Invalid status_range "${value}". Lower bound (${low}) must not exceed upper bound (${high}).`);
240
+ }
241
+ return value;
242
+ }
243
+ // Exact code — e.g. "401"
244
+ if (/^\d{3}$/.test(value)) {
245
+ const code = parseInt(value, 10);
246
+ if (code < MIN_HTTP_STATUS || code > MAX_HTTP_STATUS) {
247
+ throw new Error(`Invalid status_range "${value}". Status code must be between ${MIN_HTTP_STATUS} and ${MAX_HTTP_STATUS}.`);
248
+ }
249
+ return value;
250
+ }
251
+ throw new Error(`Invalid status_range "${value}". Expected format: Nxx (e.g. 2xx), exact code (e.g. 401), or range (e.g. 500-503).`);
252
+ }
253
+ /**
254
+ * Build a RequestFilter from optional MCP tool parameters.
255
+ */
256
+ export function buildFilter(params) {
257
+ const filter = {};
258
+ if (params.method) {
259
+ filter.methods = params.method
260
+ .split(",")
261
+ .map((m) => m.trim().toUpperCase())
262
+ .filter((m) => m.length > 0);
263
+ if (filter.methods.length === 0) {
264
+ delete filter.methods;
265
+ }
266
+ }
267
+ if (params.status_range) {
268
+ filter.statusRange = validateStatusRange(params.status_range);
269
+ }
270
+ if (params.search) {
271
+ filter.search = params.search;
272
+ }
273
+ if (params.host) {
274
+ filter.host = params.host;
275
+ }
276
+ if (params.path) {
277
+ filter.pathPrefix = params.path;
278
+ }
279
+ if (params.since) {
280
+ const ts = new Date(params.since).getTime();
281
+ if (isNaN(ts)) {
282
+ throw new Error(`Invalid since timestamp "${params.since}". Expected ISO 8601 format.`);
283
+ }
284
+ filter.since = ts;
285
+ }
286
+ if (params.before) {
287
+ const ts = new Date(params.before).getTime();
288
+ if (isNaN(ts)) {
289
+ throw new Error(`Invalid before timestamp "${params.before}". Expected ISO 8601 format.`);
290
+ }
291
+ filter.before = ts;
292
+ }
293
+ if (params.header_name) {
294
+ filter.headerName = params.header_name;
295
+ }
296
+ if (params.header_value) {
297
+ filter.headerValue = params.header_value;
298
+ }
299
+ if (params.header_target) {
300
+ filter.headerTarget = params.header_target;
301
+ }
302
+ if (params.intercepted_by) {
303
+ filter.interceptedBy = params.intercepted_by;
304
+ }
305
+ return Object.keys(filter).length > 0 ? filter : undefined;
306
+ }
307
+ /**
308
+ * Clamp a limit value to a safe range.
309
+ */
310
+ export function clampLimit(limit) {
311
+ if (limit === undefined)
312
+ return DEFAULT_LIST_LIMIT;
313
+ return Math.floor(Math.max(1, Math.min(limit, MAX_LIST_LIMIT)));
314
+ }
315
+ /**
316
+ * Create and configure the procsi MCP server.
317
+ * Returns the server instance (call `start()` to connect transport).
318
+ */
319
+ export function createProcsiMcpServer(options) {
320
+ const { projectRoot } = options;
321
+ const paths = getProcsiPaths(projectRoot);
322
+ const version = getProcsiVersion();
323
+ const server = new McpServer({
324
+ name: "procsi",
325
+ version,
326
+ });
327
+ const client = new ControlClient(paths.controlSocketFile);
328
+ // --- procsi_get_status ---
329
+ server.tool("procsi_get_status", "Get the current status of the procsi proxy daemon — running state, proxy port, session count, and total captured request count. Use this to check if the daemon is running before calling other tools.", {}, async () => {
330
+ try {
331
+ const status = await client.status();
332
+ const lines = [
333
+ `**Running:** ${status.running}`,
334
+ `**Proxy Port:** ${status.proxyPort ?? "unknown"}`,
335
+ `**Sessions:** ${status.sessionCount}`,
336
+ `**Requests Captured:** ${status.requestCount}`,
337
+ `**Version:** ${status.version}`,
338
+ ];
339
+ if (status.interceptorCount !== undefined) {
340
+ lines.push(`**Interceptors:** ${status.interceptorCount}`);
341
+ }
342
+ return textResult(lines.join("\n"));
343
+ }
344
+ catch (err) {
345
+ return textResult(`Failed to connect to procsi daemon. Is it running? Error: ${err instanceof Error ? err.message : "Unknown error"}`, true);
346
+ }
347
+ });
348
+ // --- procsi_list_requests ---
349
+ server.tool("procsi_list_requests", "Search and filter captured HTTP requests. Returns summaries (method, URL, status, timing). Supports filtering by HTTP method(s), status code (range, exact, or Nxx pattern), host, path prefix, time window, URL substring, and headers. Use procsi_get_request with a request ID to fetch full headers and bodies.", {
350
+ method: z
351
+ .string()
352
+ .optional()
353
+ .describe("Filter by HTTP method (e.g. 'GET', 'POST'). Case-insensitive. Comma-separated for multiple methods (e.g. 'GET,POST')."),
354
+ status_range: z
355
+ .string()
356
+ .optional()
357
+ .describe("Filter by status code. Accepts Nxx patterns (e.g. '2xx'), exact codes (e.g. '401'), or numeric ranges (e.g. '500-503')."),
358
+ search: z
359
+ .string()
360
+ .optional()
361
+ .describe("Case-insensitive substring match against the full URL and path."),
362
+ host: z
363
+ .string()
364
+ .optional()
365
+ .describe("Filter by host/domain. Exact match by default. Prefix with '.' for suffix matching (e.g. '.example.com' matches 'api.example.com')."),
366
+ path: z
367
+ .string()
368
+ .optional()
369
+ .describe("Filter by path prefix (e.g. '/api/v2' matches '/api/v2/users')."),
370
+ since: z
371
+ .string()
372
+ .optional()
373
+ .describe("Only include requests after this ISO 8601 timestamp (e.g. '2024-01-15T10:30:00Z')."),
374
+ before: z
375
+ .string()
376
+ .optional()
377
+ .describe("Only include requests before this ISO 8601 timestamp."),
378
+ header_name: z
379
+ .string()
380
+ .optional()
381
+ .describe("Filter by header name (case-insensitive). When used alone, matches requests that have this header. Combine with header_value for exact value matching."),
382
+ header_value: z
383
+ .string()
384
+ .optional()
385
+ .describe("Filter by header value (requires header_name). Only returns requests where the specified header has this exact value."),
386
+ header_target: z
387
+ .enum(["request", "response", "both"])
388
+ .optional()
389
+ .describe('Which headers to search: "request", "response", or "both" (default "both").'),
390
+ intercepted_by: z
391
+ .string()
392
+ .optional()
393
+ .describe("Filter by interceptor name. Only returns requests handled by this interceptor."),
394
+ limit: z
395
+ .number()
396
+ .optional()
397
+ .describe(`Max results to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT})`),
398
+ offset: z
399
+ .number()
400
+ .int()
401
+ .min(0)
402
+ .optional()
403
+ .describe("Offset for pagination (0-based, must be a non-negative integer)"),
404
+ format: FORMAT_SCHEMA,
405
+ }, async (params) => {
406
+ try {
407
+ const filter = buildFilter(params);
408
+ const limit = clampLimit(params.limit);
409
+ const [summaries, total] = await Promise.all([
410
+ client.listRequestsSummary({ limit, offset: params.offset, filter }),
411
+ client.countRequests({ filter }),
412
+ ]);
413
+ if (params.format === "json") {
414
+ return jsonResult({
415
+ total,
416
+ showing: summaries.length,
417
+ requests: summaries.map((s) => ({
418
+ ...s,
419
+ timestamp: new Date(s.timestamp).toISOString(),
420
+ })),
421
+ });
422
+ }
423
+ if (summaries.length === 0) {
424
+ if (total > 0) {
425
+ return textResult(`No requests found. (${total.toLocaleString()} total, none in current page)`);
426
+ }
427
+ return textResult("No requests found.");
428
+ }
429
+ const lines = summaries.map(formatSummary);
430
+ const header = `Showing ${summaries.length} of ${total.toLocaleString()} request(s):`;
431
+ return textResult(`${header}\n\n${lines.join("\n")}`);
432
+ }
433
+ catch (err) {
434
+ return textResult(`Failed to list requests: ${err instanceof Error ? err.message : "Unknown error"}`, true);
435
+ }
436
+ });
437
+ // --- procsi_get_request ---
438
+ server.tool("procsi_get_request", "Fetch full details of captured HTTP request(s) by ID — including all headers, request/response bodies, timing, and status. Supports comma-separated IDs for batch fetching. Get request IDs from procsi_list_requests or procsi_search_bodies.", {
439
+ id: z
440
+ .string()
441
+ .describe("The request ID (UUID), or multiple comma-separated IDs (e.g. 'id1,id2,id3')."),
442
+ format: FORMAT_SCHEMA,
443
+ }, async (params) => {
444
+ try {
445
+ const ids = params.id
446
+ .split(",")
447
+ .map((s) => s.trim())
448
+ .filter((s) => s.length > 0);
449
+ if (ids.length === 0) {
450
+ return textResult("No valid IDs provided.", true);
451
+ }
452
+ const found = [];
453
+ const notFound = [];
454
+ for (const id of ids) {
455
+ const request = await client.getRequest(id);
456
+ if (request) {
457
+ found.push(request);
458
+ }
459
+ else {
460
+ notFound.push(id);
461
+ }
462
+ }
463
+ if (params.format === "json") {
464
+ return jsonResult({
465
+ requests: found.map(serialiseRequest),
466
+ notFound,
467
+ });
468
+ }
469
+ if (found.length === 0) {
470
+ const idList = notFound.join(", ");
471
+ return textResult(`No request(s) found with ID(s): ${idList}`, true);
472
+ }
473
+ let output = found.map(formatRequest).join("\n\n---\n\n");
474
+ if (notFound.length > 0) {
475
+ output += `\n\nNot found: ${notFound.join(", ")}`;
476
+ }
477
+ return textResult(output);
478
+ }
479
+ catch (err) {
480
+ return textResult(`Failed to get request: ${err instanceof Error ? err.message : "Unknown error"}`, true);
481
+ }
482
+ });
483
+ // --- procsi_search_bodies ---
484
+ server.tool("procsi_search_bodies", "Search through request and response body content for a text substring. Only searches text-based bodies (JSON, HTML, XML, etc.), skipping binary content. Supports filtering by method(s), status code, host, path prefix, time window, and headers. Returns summaries — use procsi_get_request for full details.", {
485
+ query: z
486
+ .string()
487
+ .describe("Text to search for in request/response bodies. Case-insensitive substring match."),
488
+ method: z
489
+ .string()
490
+ .optional()
491
+ .describe("Filter by HTTP method (e.g. 'GET', 'POST'). Case-insensitive. Comma-separated for multiple methods (e.g. 'GET,POST')."),
492
+ status_range: z
493
+ .string()
494
+ .optional()
495
+ .describe("Filter by status code. Accepts Nxx patterns (e.g. '2xx'), exact codes (e.g. '401'), or numeric ranges (e.g. '500-503')."),
496
+ host: z
497
+ .string()
498
+ .optional()
499
+ .describe("Filter by host/domain. Exact match by default. Prefix with '.' for suffix matching (e.g. '.example.com' matches 'api.example.com')."),
500
+ path: z
501
+ .string()
502
+ .optional()
503
+ .describe("Filter by path prefix (e.g. '/api/v2' matches '/api/v2/users')."),
504
+ since: z
505
+ .string()
506
+ .optional()
507
+ .describe("Only include requests after this ISO 8601 timestamp (e.g. '2024-01-15T10:30:00Z')."),
508
+ before: z
509
+ .string()
510
+ .optional()
511
+ .describe("Only include requests before this ISO 8601 timestamp."),
512
+ header_name: z
513
+ .string()
514
+ .optional()
515
+ .describe("Filter by header name (case-insensitive). When used alone, matches requests that have this header. Combine with header_value for exact value matching."),
516
+ header_value: z
517
+ .string()
518
+ .optional()
519
+ .describe("Filter by header value (requires header_name). Only returns requests where the specified header has this exact value."),
520
+ header_target: z
521
+ .enum(["request", "response", "both"])
522
+ .optional()
523
+ .describe('Which headers to search: "request", "response", or "both" (default "both").'),
524
+ intercepted_by: z
525
+ .string()
526
+ .optional()
527
+ .describe("Filter by interceptor name. Only returns requests handled by this interceptor."),
528
+ limit: z
529
+ .number()
530
+ .optional()
531
+ .describe(`Max results to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT})`),
532
+ offset: z
533
+ .number()
534
+ .int()
535
+ .min(0)
536
+ .optional()
537
+ .describe("Offset for pagination (0-based, must be a non-negative integer)"),
538
+ format: FORMAT_SCHEMA,
539
+ }, async (params) => {
540
+ try {
541
+ const filter = buildFilter(params);
542
+ const summaries = await client.searchBodies({
543
+ query: params.query,
544
+ limit: clampLimit(params.limit),
545
+ offset: params.offset,
546
+ filter,
547
+ });
548
+ if (params.format === "json") {
549
+ return jsonResult({
550
+ query: params.query,
551
+ total: summaries.length,
552
+ requests: summaries.map((s) => ({
553
+ ...s,
554
+ timestamp: new Date(s.timestamp).toISOString(),
555
+ })),
556
+ });
557
+ }
558
+ if (summaries.length === 0) {
559
+ return textResult(`No requests found with body content matching: "${params.query}"`);
560
+ }
561
+ const lines = summaries.map(formatSummary);
562
+ const header = `Found ${summaries.length} request(s) with body content matching "${params.query}":`;
563
+ return textResult(`${header}\n\n${lines.join("\n")}`);
564
+ }
565
+ catch (err) {
566
+ return textResult(`Failed to search bodies: ${err instanceof Error ? err.message : "Unknown error"}`, true);
567
+ }
568
+ });
569
+ // --- procsi_count_requests ---
570
+ server.tool("procsi_count_requests", "Count captured HTTP requests, optionally filtered. Useful for checking total traffic volume or verifying how many requests match a filter before paginating through them.", {
571
+ method: z
572
+ .string()
573
+ .optional()
574
+ .describe("Filter by HTTP method (e.g. 'GET', 'POST'). Case-insensitive. Comma-separated for multiple methods (e.g. 'GET,POST')."),
575
+ status_range: z
576
+ .string()
577
+ .optional()
578
+ .describe("Filter by status code. Accepts Nxx patterns (e.g. '2xx'), exact codes (e.g. '401'), or numeric ranges (e.g. '500-503')."),
579
+ search: z
580
+ .string()
581
+ .optional()
582
+ .describe("Case-insensitive substring match against the full URL and path."),
583
+ host: z
584
+ .string()
585
+ .optional()
586
+ .describe("Filter by host/domain. Exact match by default. Prefix with '.' for suffix matching (e.g. '.example.com' matches 'api.example.com')."),
587
+ path: z
588
+ .string()
589
+ .optional()
590
+ .describe("Filter by path prefix (e.g. '/api/v2' matches '/api/v2/users')."),
591
+ since: z
592
+ .string()
593
+ .optional()
594
+ .describe("Only include requests after this ISO 8601 timestamp (e.g. '2024-01-15T10:30:00Z')."),
595
+ before: z
596
+ .string()
597
+ .optional()
598
+ .describe("Only include requests before this ISO 8601 timestamp."),
599
+ header_name: z
600
+ .string()
601
+ .optional()
602
+ .describe("Filter by header name (case-insensitive). When used alone, matches requests that have this header. Combine with header_value for exact value matching."),
603
+ header_value: z
604
+ .string()
605
+ .optional()
606
+ .describe("Filter by header value (requires header_name). Only returns requests where the specified header has this exact value."),
607
+ header_target: z
608
+ .enum(["request", "response", "both"])
609
+ .optional()
610
+ .describe('Which headers to search: "request", "response", or "both" (default "both").'),
611
+ intercepted_by: z
612
+ .string()
613
+ .optional()
614
+ .describe("Filter by interceptor name. Only returns requests handled by this interceptor."),
615
+ format: FORMAT_SCHEMA,
616
+ }, async (params) => {
617
+ try {
618
+ const filter = buildFilter(params);
619
+ const count = await client.countRequests({ filter });
620
+ if (params.format === "json") {
621
+ return jsonResult({ count });
622
+ }
623
+ return textResult(`${count} request(s)`);
624
+ }
625
+ catch (err) {
626
+ return textResult(`Failed to count requests: ${err instanceof Error ? err.message : "Unknown error"}`, true);
627
+ }
628
+ });
629
+ // --- procsi_query_json ---
630
+ server.tool("procsi_query_json", "Extract values from JSON request/response bodies using JSONPath syntax (SQLite json_extract). Only queries bodies with JSON content types. Use this to find requests where a specific JSON field exists or has a particular value.", {
631
+ json_path: z
632
+ .string()
633
+ .describe("JSONPath expression to extract (e.g. '$.user.name', '$.items[0].id', '$.status'). Uses SQLite json_extract syntax."),
634
+ value: z
635
+ .string()
636
+ .optional()
637
+ .describe("Only return requests where the extracted value equals this string. Useful for finding requests with specific field values."),
638
+ target: z
639
+ .enum(["request", "response", "both"])
640
+ .optional()
641
+ .describe('Which body to query: "request", "response", or "both" (default "both"). When "both", prefers the request body value.'),
642
+ method: z
643
+ .string()
644
+ .optional()
645
+ .describe("Filter by HTTP method (e.g. 'GET', 'POST'). Case-insensitive. Comma-separated for multiple methods (e.g. 'GET,POST')."),
646
+ status_range: z
647
+ .string()
648
+ .optional()
649
+ .describe("Filter by status code. Accepts Nxx patterns (e.g. '2xx'), exact codes (e.g. '401'), or numeric ranges (e.g. '500-503')."),
650
+ host: z
651
+ .string()
652
+ .optional()
653
+ .describe("Filter by host/domain. Exact match by default. Prefix with '.' for suffix matching (e.g. '.example.com' matches 'api.example.com')."),
654
+ path: z
655
+ .string()
656
+ .optional()
657
+ .describe("Filter by path prefix (e.g. '/api/v2' matches '/api/v2/users')."),
658
+ since: z
659
+ .string()
660
+ .optional()
661
+ .describe("Only include requests after this ISO 8601 timestamp (e.g. '2024-01-15T10:30:00Z')."),
662
+ before: z
663
+ .string()
664
+ .optional()
665
+ .describe("Only include requests before this ISO 8601 timestamp."),
666
+ header_name: z.string().optional().describe("Filter by header name (case-insensitive)."),
667
+ header_value: z
668
+ .string()
669
+ .optional()
670
+ .describe("Filter by header value (requires header_name)."),
671
+ header_target: z
672
+ .enum(["request", "response", "both"])
673
+ .optional()
674
+ .describe('Which headers to search: "request", "response", or "both" (default "both").'),
675
+ limit: z
676
+ .number()
677
+ .optional()
678
+ .describe(`Max results to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT})`),
679
+ offset: z
680
+ .number()
681
+ .int()
682
+ .min(0)
683
+ .optional()
684
+ .describe("Offset for pagination (0-based, must be a non-negative integer)"),
685
+ format: FORMAT_SCHEMA,
686
+ }, async (params) => {
687
+ try {
688
+ const filter = buildFilter(params);
689
+ const results = await client.queryJsonBodies({
690
+ jsonPath: params.json_path,
691
+ value: params.value,
692
+ target: params.target,
693
+ limit: clampLimit(params.limit),
694
+ offset: params.offset,
695
+ filter,
696
+ });
697
+ if (params.format === "json") {
698
+ return jsonResult({
699
+ json_path: params.json_path,
700
+ total: results.length,
701
+ results: results.map((r) => ({
702
+ ...r,
703
+ timestamp: new Date(r.timestamp).toISOString(),
704
+ })),
705
+ });
706
+ }
707
+ if (results.length === 0) {
708
+ return textResult(`No JSON bodies found with path: "${params.json_path}"`);
709
+ }
710
+ const lines = results.map((r) => {
711
+ const summary = formatSummary(r);
712
+ const valueStr = typeof r.extractedValue === "string"
713
+ ? r.extractedValue
714
+ : JSON.stringify(r.extractedValue);
715
+ return `${summary} → ${params.json_path}=${valueStr}`;
716
+ });
717
+ const header = `Found ${results.length} request(s) with JSON path "${params.json_path}":`;
718
+ return textResult(`${header}\n\n${lines.join("\n")}`);
719
+ }
720
+ catch (err) {
721
+ return textResult(`Failed to query JSON bodies: ${err instanceof Error ? err.message : "Unknown error"}`, true);
722
+ }
723
+ });
724
+ // --- procsi_clear_requests ---
725
+ server.tool("procsi_clear_requests", "Clear all captured HTTP requests from storage. This is irreversible.", {}, async () => {
726
+ try {
727
+ await client.clearRequests();
728
+ return textResult("All requests cleared.");
729
+ }
730
+ catch (err) {
731
+ return textResult(`Failed to clear requests: ${err instanceof Error ? err.message : "Unknown error"}`, true);
732
+ }
733
+ });
734
+ // --- procsi_list_sessions ---
735
+ server.tool("procsi_list_sessions", "List all active proxy sessions. Each session represents a process that registered with the daemon (e.g. a shell running `eval $(procsi vars)`).", {}, async () => {
736
+ try {
737
+ const sessions = await client.listSessions();
738
+ if (sessions.length === 0) {
739
+ return textResult("No active sessions.");
740
+ }
741
+ const lines = sessions.map(formatSession);
742
+ const header = `${sessions.length} session(s):`;
743
+ return textResult(`${header}\n\n${lines.join("\n")}`);
744
+ }
745
+ catch (err) {
746
+ return textResult(`Failed to list sessions: ${err instanceof Error ? err.message : "Unknown error"}`, true);
747
+ }
748
+ });
749
+ // --- procsi_list_interceptors ---
750
+ server.tool("procsi_list_interceptors", "List all loaded interceptors — their names, source files, whether they have a match function, and any load errors. Use this to check which interceptors are active.", {
751
+ format: FORMAT_SCHEMA,
752
+ }, async (params) => {
753
+ try {
754
+ const interceptors = await client.listInterceptors();
755
+ if (params.format === "json") {
756
+ return jsonResult(interceptors);
757
+ }
758
+ if (interceptors.length === 0) {
759
+ return textResult("No interceptors loaded.");
760
+ }
761
+ const lines = interceptors.map(formatInterceptor);
762
+ const header = `${interceptors.length} interceptor(s):`;
763
+ return textResult(`${header}\n\n${lines.join("\n")}`);
764
+ }
765
+ catch (err) {
766
+ return textResult(`Failed to list interceptors: ${err instanceof Error ? err.message : "Unknown error"}`, true);
767
+ }
768
+ });
769
+ // --- procsi_reload_interceptors ---
770
+ server.tool("procsi_reload_interceptors", "Reload interceptors from disk. Use after editing interceptor files to apply changes without restarting the daemon.", {
771
+ format: FORMAT_SCHEMA,
772
+ }, async (params) => {
773
+ try {
774
+ const result = await client.reloadInterceptors();
775
+ if (params.format === "json") {
776
+ return jsonResult(result);
777
+ }
778
+ if (!result.success) {
779
+ return textResult(result.error ?? "Reload failed", true);
780
+ }
781
+ return textResult(`Interceptors reloaded successfully. ${result.count} interceptor(s) loaded.`);
782
+ }
783
+ catch (err) {
784
+ return textResult(`Failed to reload interceptors: ${err instanceof Error ? err.message : "Unknown error"}`, true);
785
+ }
786
+ });
787
+ return {
788
+ server,
789
+ client,
790
+ /**
791
+ * Start the MCP server with stdio transport.
792
+ */
793
+ async start() {
794
+ const transport = new StdioServerTransport();
795
+ await server.connect(transport);
796
+ },
797
+ /**
798
+ * Shut down cleanly.
799
+ */
800
+ async close() {
801
+ client.close();
802
+ await server.close();
803
+ },
804
+ };
805
+ }
806
+ //# sourceMappingURL=server.js.map