gitx.do 0.1.1 → 0.1.2

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 (356) hide show
  1. package/README.md +40 -353
  2. package/dist/do/logger.d.ts +50 -0
  3. package/dist/do/logger.d.ts.map +1 -0
  4. package/dist/do/logger.js +122 -0
  5. package/dist/do/logger.js.map +1 -0
  6. package/dist/{durable-object → do}/schema.d.ts +3 -3
  7. package/dist/do/schema.d.ts.map +1 -0
  8. package/dist/{durable-object → do}/schema.js +4 -3
  9. package/dist/do/schema.js.map +1 -0
  10. package/dist/do/types.d.ts +267 -0
  11. package/dist/do/types.d.ts.map +1 -0
  12. package/dist/do/types.js +62 -0
  13. package/dist/do/types.js.map +1 -0
  14. package/dist/index.d.ts +14 -469
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +31 -483
  17. package/dist/index.js.map +1 -1
  18. package/package.json +13 -21
  19. package/dist/cli/commands/add.d.ts +0 -176
  20. package/dist/cli/commands/add.d.ts.map +0 -1
  21. package/dist/cli/commands/add.js +0 -979
  22. package/dist/cli/commands/add.js.map +0 -1
  23. package/dist/cli/commands/blame.d.ts +0 -259
  24. package/dist/cli/commands/blame.d.ts.map +0 -1
  25. package/dist/cli/commands/blame.js +0 -609
  26. package/dist/cli/commands/blame.js.map +0 -1
  27. package/dist/cli/commands/branch.d.ts +0 -249
  28. package/dist/cli/commands/branch.d.ts.map +0 -1
  29. package/dist/cli/commands/branch.js +0 -693
  30. package/dist/cli/commands/branch.js.map +0 -1
  31. package/dist/cli/commands/checkout.d.ts +0 -73
  32. package/dist/cli/commands/checkout.d.ts.map +0 -1
  33. package/dist/cli/commands/checkout.js +0 -725
  34. package/dist/cli/commands/checkout.js.map +0 -1
  35. package/dist/cli/commands/commit.d.ts +0 -182
  36. package/dist/cli/commands/commit.d.ts.map +0 -1
  37. package/dist/cli/commands/commit.js +0 -457
  38. package/dist/cli/commands/commit.js.map +0 -1
  39. package/dist/cli/commands/diff.d.ts +0 -464
  40. package/dist/cli/commands/diff.d.ts.map +0 -1
  41. package/dist/cli/commands/diff.js +0 -959
  42. package/dist/cli/commands/diff.js.map +0 -1
  43. package/dist/cli/commands/log.d.ts +0 -239
  44. package/dist/cli/commands/log.d.ts.map +0 -1
  45. package/dist/cli/commands/log.js +0 -535
  46. package/dist/cli/commands/log.js.map +0 -1
  47. package/dist/cli/commands/merge.d.ts +0 -106
  48. package/dist/cli/commands/merge.d.ts.map +0 -1
  49. package/dist/cli/commands/merge.js +0 -852
  50. package/dist/cli/commands/merge.js.map +0 -1
  51. package/dist/cli/commands/review.d.ts +0 -457
  52. package/dist/cli/commands/review.d.ts.map +0 -1
  53. package/dist/cli/commands/review.js +0 -558
  54. package/dist/cli/commands/review.js.map +0 -1
  55. package/dist/cli/commands/stash.d.ts +0 -157
  56. package/dist/cli/commands/stash.d.ts.map +0 -1
  57. package/dist/cli/commands/stash.js +0 -655
  58. package/dist/cli/commands/stash.js.map +0 -1
  59. package/dist/cli/commands/status.d.ts +0 -269
  60. package/dist/cli/commands/status.d.ts.map +0 -1
  61. package/dist/cli/commands/status.js +0 -492
  62. package/dist/cli/commands/status.js.map +0 -1
  63. package/dist/cli/commands/web.d.ts +0 -199
  64. package/dist/cli/commands/web.d.ts.map +0 -1
  65. package/dist/cli/commands/web.js +0 -697
  66. package/dist/cli/commands/web.js.map +0 -1
  67. package/dist/cli/fs-adapter.d.ts +0 -656
  68. package/dist/cli/fs-adapter.d.ts.map +0 -1
  69. package/dist/cli/fs-adapter.js +0 -1177
  70. package/dist/cli/fs-adapter.js.map +0 -1
  71. package/dist/cli/fsx-cli-adapter.d.ts +0 -359
  72. package/dist/cli/fsx-cli-adapter.d.ts.map +0 -1
  73. package/dist/cli/fsx-cli-adapter.js +0 -619
  74. package/dist/cli/fsx-cli-adapter.js.map +0 -1
  75. package/dist/cli/index.d.ts +0 -387
  76. package/dist/cli/index.d.ts.map +0 -1
  77. package/dist/cli/index.js +0 -579
  78. package/dist/cli/index.js.map +0 -1
  79. package/dist/cli/ui/components/DiffView.d.ts +0 -12
  80. package/dist/cli/ui/components/DiffView.d.ts.map +0 -1
  81. package/dist/cli/ui/components/DiffView.js +0 -11
  82. package/dist/cli/ui/components/DiffView.js.map +0 -1
  83. package/dist/cli/ui/components/ErrorDisplay.d.ts +0 -10
  84. package/dist/cli/ui/components/ErrorDisplay.d.ts.map +0 -1
  85. package/dist/cli/ui/components/ErrorDisplay.js +0 -11
  86. package/dist/cli/ui/components/ErrorDisplay.js.map +0 -1
  87. package/dist/cli/ui/components/FuzzySearch.d.ts +0 -15
  88. package/dist/cli/ui/components/FuzzySearch.d.ts.map +0 -1
  89. package/dist/cli/ui/components/FuzzySearch.js +0 -12
  90. package/dist/cli/ui/components/FuzzySearch.js.map +0 -1
  91. package/dist/cli/ui/components/LoadingSpinner.d.ts +0 -10
  92. package/dist/cli/ui/components/LoadingSpinner.d.ts.map +0 -1
  93. package/dist/cli/ui/components/LoadingSpinner.js +0 -10
  94. package/dist/cli/ui/components/LoadingSpinner.js.map +0 -1
  95. package/dist/cli/ui/components/NavigationList.d.ts +0 -14
  96. package/dist/cli/ui/components/NavigationList.d.ts.map +0 -1
  97. package/dist/cli/ui/components/NavigationList.js +0 -11
  98. package/dist/cli/ui/components/NavigationList.js.map +0 -1
  99. package/dist/cli/ui/components/ScrollableContent.d.ts +0 -13
  100. package/dist/cli/ui/components/ScrollableContent.d.ts.map +0 -1
  101. package/dist/cli/ui/components/ScrollableContent.js +0 -11
  102. package/dist/cli/ui/components/ScrollableContent.js.map +0 -1
  103. package/dist/cli/ui/components/index.d.ts +0 -7
  104. package/dist/cli/ui/components/index.d.ts.map +0 -1
  105. package/dist/cli/ui/components/index.js +0 -9
  106. package/dist/cli/ui/components/index.js.map +0 -1
  107. package/dist/cli/ui/terminal-ui.d.ts +0 -85
  108. package/dist/cli/ui/terminal-ui.d.ts.map +0 -1
  109. package/dist/cli/ui/terminal-ui.js +0 -121
  110. package/dist/cli/ui/terminal-ui.js.map +0 -1
  111. package/dist/do/BashModule.d.ts +0 -871
  112. package/dist/do/BashModule.d.ts.map +0 -1
  113. package/dist/do/BashModule.js +0 -1143
  114. package/dist/do/BashModule.js.map +0 -1
  115. package/dist/do/FsModule.d.ts +0 -612
  116. package/dist/do/FsModule.d.ts.map +0 -1
  117. package/dist/do/FsModule.js +0 -1120
  118. package/dist/do/FsModule.js.map +0 -1
  119. package/dist/do/GitModule.d.ts +0 -635
  120. package/dist/do/GitModule.d.ts.map +0 -1
  121. package/dist/do/GitModule.js +0 -784
  122. package/dist/do/GitModule.js.map +0 -1
  123. package/dist/do/GitRepoDO.d.ts +0 -281
  124. package/dist/do/GitRepoDO.d.ts.map +0 -1
  125. package/dist/do/GitRepoDO.js +0 -479
  126. package/dist/do/GitRepoDO.js.map +0 -1
  127. package/dist/do/bash-ast.d.ts +0 -246
  128. package/dist/do/bash-ast.d.ts.map +0 -1
  129. package/dist/do/bash-ast.js +0 -888
  130. package/dist/do/bash-ast.js.map +0 -1
  131. package/dist/do/container-executor.d.ts +0 -491
  132. package/dist/do/container-executor.d.ts.map +0 -1
  133. package/dist/do/container-executor.js +0 -731
  134. package/dist/do/container-executor.js.map +0 -1
  135. package/dist/do/index.d.ts +0 -53
  136. package/dist/do/index.d.ts.map +0 -1
  137. package/dist/do/index.js +0 -91
  138. package/dist/do/index.js.map +0 -1
  139. package/dist/do/tiered-storage.d.ts +0 -403
  140. package/dist/do/tiered-storage.d.ts.map +0 -1
  141. package/dist/do/tiered-storage.js +0 -689
  142. package/dist/do/tiered-storage.js.map +0 -1
  143. package/dist/do/withBash.d.ts +0 -231
  144. package/dist/do/withBash.d.ts.map +0 -1
  145. package/dist/do/withBash.js +0 -244
  146. package/dist/do/withBash.js.map +0 -1
  147. package/dist/do/withFs.d.ts +0 -237
  148. package/dist/do/withFs.d.ts.map +0 -1
  149. package/dist/do/withFs.js +0 -387
  150. package/dist/do/withFs.js.map +0 -1
  151. package/dist/do/withGit.d.ts +0 -180
  152. package/dist/do/withGit.d.ts.map +0 -1
  153. package/dist/do/withGit.js +0 -271
  154. package/dist/do/withGit.js.map +0 -1
  155. package/dist/durable-object/object-store.d.ts +0 -633
  156. package/dist/durable-object/object-store.d.ts.map +0 -1
  157. package/dist/durable-object/object-store.js +0 -1164
  158. package/dist/durable-object/object-store.js.map +0 -1
  159. package/dist/durable-object/schema.d.ts.map +0 -1
  160. package/dist/durable-object/schema.js.map +0 -1
  161. package/dist/durable-object/wal.d.ts +0 -416
  162. package/dist/durable-object/wal.d.ts.map +0 -1
  163. package/dist/durable-object/wal.js +0 -445
  164. package/dist/durable-object/wal.js.map +0 -1
  165. package/dist/mcp/adapter.d.ts +0 -772
  166. package/dist/mcp/adapter.d.ts.map +0 -1
  167. package/dist/mcp/adapter.js +0 -895
  168. package/dist/mcp/adapter.js.map +0 -1
  169. package/dist/mcp/sandbox/miniflare-evaluator.d.ts +0 -22
  170. package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +0 -1
  171. package/dist/mcp/sandbox/miniflare-evaluator.js +0 -140
  172. package/dist/mcp/sandbox/miniflare-evaluator.js.map +0 -1
  173. package/dist/mcp/sandbox/object-store-proxy.d.ts +0 -32
  174. package/dist/mcp/sandbox/object-store-proxy.d.ts.map +0 -1
  175. package/dist/mcp/sandbox/object-store-proxy.js +0 -30
  176. package/dist/mcp/sandbox/object-store-proxy.js.map +0 -1
  177. package/dist/mcp/sandbox/template.d.ts +0 -17
  178. package/dist/mcp/sandbox/template.d.ts.map +0 -1
  179. package/dist/mcp/sandbox/template.js +0 -71
  180. package/dist/mcp/sandbox/template.js.map +0 -1
  181. package/dist/mcp/sandbox.d.ts +0 -764
  182. package/dist/mcp/sandbox.d.ts.map +0 -1
  183. package/dist/mcp/sandbox.js +0 -1362
  184. package/dist/mcp/sandbox.js.map +0 -1
  185. package/dist/mcp/sdk-adapter.d.ts +0 -835
  186. package/dist/mcp/sdk-adapter.d.ts.map +0 -1
  187. package/dist/mcp/sdk-adapter.js +0 -974
  188. package/dist/mcp/sdk-adapter.js.map +0 -1
  189. package/dist/mcp/tools/do.d.ts +0 -32
  190. package/dist/mcp/tools/do.d.ts.map +0 -1
  191. package/dist/mcp/tools/do.js +0 -117
  192. package/dist/mcp/tools/do.js.map +0 -1
  193. package/dist/mcp/tools.d.ts +0 -548
  194. package/dist/mcp/tools.d.ts.map +0 -1
  195. package/dist/mcp/tools.js +0 -3170
  196. package/dist/mcp/tools.js.map +0 -1
  197. package/dist/ops/blame.d.ts +0 -551
  198. package/dist/ops/blame.d.ts.map +0 -1
  199. package/dist/ops/blame.js +0 -1037
  200. package/dist/ops/blame.js.map +0 -1
  201. package/dist/ops/branch.d.ts +0 -766
  202. package/dist/ops/branch.d.ts.map +0 -1
  203. package/dist/ops/branch.js +0 -950
  204. package/dist/ops/branch.js.map +0 -1
  205. package/dist/ops/commit-traversal.d.ts +0 -349
  206. package/dist/ops/commit-traversal.d.ts.map +0 -1
  207. package/dist/ops/commit-traversal.js +0 -821
  208. package/dist/ops/commit-traversal.js.map +0 -1
  209. package/dist/ops/commit.d.ts +0 -555
  210. package/dist/ops/commit.d.ts.map +0 -1
  211. package/dist/ops/commit.js +0 -826
  212. package/dist/ops/commit.js.map +0 -1
  213. package/dist/ops/merge-base.d.ts +0 -397
  214. package/dist/ops/merge-base.d.ts.map +0 -1
  215. package/dist/ops/merge-base.js +0 -691
  216. package/dist/ops/merge-base.js.map +0 -1
  217. package/dist/ops/merge.d.ts +0 -855
  218. package/dist/ops/merge.d.ts.map +0 -1
  219. package/dist/ops/merge.js +0 -1551
  220. package/dist/ops/merge.js.map +0 -1
  221. package/dist/ops/tag.d.ts +0 -247
  222. package/dist/ops/tag.d.ts.map +0 -1
  223. package/dist/ops/tag.js +0 -649
  224. package/dist/ops/tag.js.map +0 -1
  225. package/dist/ops/tree-builder.d.ts +0 -178
  226. package/dist/ops/tree-builder.d.ts.map +0 -1
  227. package/dist/ops/tree-builder.js +0 -271
  228. package/dist/ops/tree-builder.js.map +0 -1
  229. package/dist/ops/tree-diff.d.ts +0 -291
  230. package/dist/ops/tree-diff.d.ts.map +0 -1
  231. package/dist/ops/tree-diff.js +0 -705
  232. package/dist/ops/tree-diff.js.map +0 -1
  233. package/dist/pack/delta.d.ts +0 -248
  234. package/dist/pack/delta.d.ts.map +0 -1
  235. package/dist/pack/delta.js +0 -740
  236. package/dist/pack/delta.js.map +0 -1
  237. package/dist/pack/format.d.ts +0 -446
  238. package/dist/pack/format.d.ts.map +0 -1
  239. package/dist/pack/format.js +0 -572
  240. package/dist/pack/format.js.map +0 -1
  241. package/dist/pack/full-generation.d.ts +0 -612
  242. package/dist/pack/full-generation.d.ts.map +0 -1
  243. package/dist/pack/full-generation.js +0 -1378
  244. package/dist/pack/full-generation.js.map +0 -1
  245. package/dist/pack/generation.d.ts +0 -441
  246. package/dist/pack/generation.d.ts.map +0 -1
  247. package/dist/pack/generation.js +0 -707
  248. package/dist/pack/generation.js.map +0 -1
  249. package/dist/pack/index.d.ts +0 -502
  250. package/dist/pack/index.d.ts.map +0 -1
  251. package/dist/pack/index.js +0 -833
  252. package/dist/pack/index.js.map +0 -1
  253. package/dist/refs/branch.d.ts +0 -683
  254. package/dist/refs/branch.d.ts.map +0 -1
  255. package/dist/refs/branch.js +0 -881
  256. package/dist/refs/branch.js.map +0 -1
  257. package/dist/refs/storage.d.ts +0 -833
  258. package/dist/refs/storage.d.ts.map +0 -1
  259. package/dist/refs/storage.js +0 -1023
  260. package/dist/refs/storage.js.map +0 -1
  261. package/dist/refs/tag.d.ts +0 -860
  262. package/dist/refs/tag.d.ts.map +0 -1
  263. package/dist/refs/tag.js +0 -996
  264. package/dist/refs/tag.js.map +0 -1
  265. package/dist/storage/backend.d.ts +0 -425
  266. package/dist/storage/backend.d.ts.map +0 -1
  267. package/dist/storage/backend.js +0 -41
  268. package/dist/storage/backend.js.map +0 -1
  269. package/dist/storage/fsx-adapter.d.ts +0 -204
  270. package/dist/storage/fsx-adapter.d.ts.map +0 -1
  271. package/dist/storage/fsx-adapter.js +0 -518
  272. package/dist/storage/fsx-adapter.js.map +0 -1
  273. package/dist/storage/lru-cache.d.ts +0 -691
  274. package/dist/storage/lru-cache.d.ts.map +0 -1
  275. package/dist/storage/lru-cache.js +0 -813
  276. package/dist/storage/lru-cache.js.map +0 -1
  277. package/dist/storage/object-index.d.ts +0 -585
  278. package/dist/storage/object-index.d.ts.map +0 -1
  279. package/dist/storage/object-index.js +0 -532
  280. package/dist/storage/object-index.js.map +0 -1
  281. package/dist/storage/r2-pack.d.ts +0 -1257
  282. package/dist/storage/r2-pack.d.ts.map +0 -1
  283. package/dist/storage/r2-pack.js +0 -1773
  284. package/dist/storage/r2-pack.js.map +0 -1
  285. package/dist/tiered/cdc-pipeline.d.ts +0 -1888
  286. package/dist/tiered/cdc-pipeline.d.ts.map +0 -1
  287. package/dist/tiered/cdc-pipeline.js +0 -1880
  288. package/dist/tiered/cdc-pipeline.js.map +0 -1
  289. package/dist/tiered/migration.d.ts +0 -1104
  290. package/dist/tiered/migration.d.ts.map +0 -1
  291. package/dist/tiered/migration.js +0 -1217
  292. package/dist/tiered/migration.js.map +0 -1
  293. package/dist/tiered/parquet-writer.d.ts +0 -1145
  294. package/dist/tiered/parquet-writer.d.ts.map +0 -1
  295. package/dist/tiered/parquet-writer.js +0 -1183
  296. package/dist/tiered/parquet-writer.js.map +0 -1
  297. package/dist/tiered/read-path.d.ts +0 -835
  298. package/dist/tiered/read-path.d.ts.map +0 -1
  299. package/dist/tiered/read-path.js +0 -487
  300. package/dist/tiered/read-path.js.map +0 -1
  301. package/dist/types/capability.d.ts +0 -1385
  302. package/dist/types/capability.d.ts.map +0 -1
  303. package/dist/types/capability.js +0 -36
  304. package/dist/types/capability.js.map +0 -1
  305. package/dist/types/index.d.ts +0 -13
  306. package/dist/types/index.d.ts.map +0 -1
  307. package/dist/types/index.js +0 -18
  308. package/dist/types/index.js.map +0 -1
  309. package/dist/types/interfaces.d.ts +0 -673
  310. package/dist/types/interfaces.d.ts.map +0 -1
  311. package/dist/types/interfaces.js +0 -26
  312. package/dist/types/interfaces.js.map +0 -1
  313. package/dist/types/objects.d.ts +0 -692
  314. package/dist/types/objects.d.ts.map +0 -1
  315. package/dist/types/objects.js +0 -837
  316. package/dist/types/objects.js.map +0 -1
  317. package/dist/types/storage.d.ts +0 -603
  318. package/dist/types/storage.d.ts.map +0 -1
  319. package/dist/types/storage.js +0 -191
  320. package/dist/types/storage.js.map +0 -1
  321. package/dist/types/worker-loader.d.ts +0 -60
  322. package/dist/types/worker-loader.d.ts.map +0 -1
  323. package/dist/types/worker-loader.js +0 -62
  324. package/dist/types/worker-loader.js.map +0 -1
  325. package/dist/utils/hash.d.ts +0 -198
  326. package/dist/utils/hash.d.ts.map +0 -1
  327. package/dist/utils/hash.js +0 -272
  328. package/dist/utils/hash.js.map +0 -1
  329. package/dist/utils/sha1.d.ts +0 -325
  330. package/dist/utils/sha1.d.ts.map +0 -1
  331. package/dist/utils/sha1.js +0 -635
  332. package/dist/utils/sha1.js.map +0 -1
  333. package/dist/wire/capabilities.d.ts +0 -1044
  334. package/dist/wire/capabilities.d.ts.map +0 -1
  335. package/dist/wire/capabilities.js +0 -941
  336. package/dist/wire/capabilities.js.map +0 -1
  337. package/dist/wire/path-security.d.ts +0 -157
  338. package/dist/wire/path-security.d.ts.map +0 -1
  339. package/dist/wire/path-security.js +0 -307
  340. package/dist/wire/path-security.js.map +0 -1
  341. package/dist/wire/pkt-line.d.ts +0 -345
  342. package/dist/wire/pkt-line.d.ts.map +0 -1
  343. package/dist/wire/pkt-line.js +0 -381
  344. package/dist/wire/pkt-line.js.map +0 -1
  345. package/dist/wire/receive-pack.d.ts +0 -1059
  346. package/dist/wire/receive-pack.d.ts.map +0 -1
  347. package/dist/wire/receive-pack.js +0 -1414
  348. package/dist/wire/receive-pack.js.map +0 -1
  349. package/dist/wire/smart-http.d.ts +0 -799
  350. package/dist/wire/smart-http.d.ts.map +0 -1
  351. package/dist/wire/smart-http.js +0 -945
  352. package/dist/wire/smart-http.js.map +0 -1
  353. package/dist/wire/upload-pack.d.ts +0 -727
  354. package/dist/wire/upload-pack.d.ts.map +0 -1
  355. package/dist/wire/upload-pack.js +0 -1141
  356. package/dist/wire/upload-pack.js.map +0 -1
package/dist/mcp/tools.js DELETED
@@ -1,3170 +0,0 @@
1
- /**
2
- * @fileoverview MCP (Model Context Protocol) Git Tool Definitions
3
- *
4
- * This module provides tool definitions for git operations that can be
5
- * exposed via the Model Context Protocol for AI assistants. It defines
6
- * a comprehensive set of git tools including status, log, diff, commit,
7
- * branch, checkout, push, pull, clone, init, add, reset, merge, rebase,
8
- * stash, tag, remote, and fetch operations.
9
- *
10
- * The module uses a registry pattern for tool management, allowing dynamic
11
- * registration, validation, and invocation of tools. Each tool follows the
12
- * MCP specification with JSON Schema input validation and standardized
13
- * result formatting.
14
- *
15
- * @module mcp/tools
16
- *
17
- * @example
18
- * // Setting up repository context and invoking a tool
19
- * import { setRepositoryContext, invokeTool } from './tools'
20
- *
21
- * // Set up the repository context first
22
- * setRepositoryContext({
23
- * objectStore: myObjectStore,
24
- * refStore: myRefStore,
25
- * index: myIndex
26
- * })
27
- *
28
- * // Invoke a tool
29
- * const result = await invokeTool('git_status', { short: true })
30
- * console.log(result.content[0].text)
31
- *
32
- * @example
33
- * // Registering a custom tool
34
- * import { registerTool } from './tools'
35
- *
36
- * registerTool({
37
- * name: 'my_custom_tool',
38
- * description: 'A custom tool',
39
- * inputSchema: { type: 'object', properties: {} },
40
- * handler: async (params) => ({
41
- * content: [{ type: 'text', text: 'Hello!' }]
42
- * })
43
- * })
44
- */
45
- import { execSync } from 'child_process';
46
- import { walkCommits } from '../ops/commit-traversal';
47
- import { DiffStatus, diffTrees } from '../ops/tree-diff';
48
- import { listBranches, createBranch, deleteBranch, getCurrentBranch } from '../ops/branch';
49
- import { createCommit } from '../ops/commit';
50
- /**
51
- * Execute a git command and return the output.
52
- *
53
- * @description Helper function to execute git CLI commands synchronously.
54
- * Used by bash CLI-based MCP tools.
55
- *
56
- * @param args - Array of arguments to pass to git
57
- * @param cwd - Working directory for the command
58
- * @returns Object with stdout, stderr, and exitCode
59
- */
60
- function execGit(args, cwd) {
61
- try {
62
- const stdout = execSync(['git', ...args].join(' '), {
63
- cwd: cwd || process.cwd(),
64
- encoding: 'utf8',
65
- stdio: ['pipe', 'pipe', 'pipe'],
66
- maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large outputs
67
- });
68
- return { stdout: stdout.toString(), stderr: '', exitCode: 0 };
69
- }
70
- catch (error) {
71
- const execError = error;
72
- return {
73
- stdout: execError.stdout?.toString() || '',
74
- stderr: execError.stderr?.toString() || '',
75
- exitCode: execError.status || 1
76
- };
77
- }
78
- }
79
- /**
80
- * Recursively flatten a tree object into a map of path -> entry.
81
- * @param objectStore - Object store for fetching trees
82
- * @param treeSha - SHA of the tree to flatten
83
- * @param prefix - Path prefix for entries
84
- * @returns Map of full paths to tree entries
85
- */
86
- async function flattenTree(objectStore, treeSha, prefix = '') {
87
- const result = new Map();
88
- const tree = await objectStore.getTree(treeSha);
89
- if (!tree)
90
- return result;
91
- for (const entry of tree.entries) {
92
- const fullPath = prefix ? `${prefix}/${entry.name}` : entry.name;
93
- if (entry.mode === '040000') {
94
- // Recursively process subdirectory
95
- const subEntries = await flattenTree(objectStore, entry.sha, fullPath);
96
- for (const [path, subEntry] of subEntries) {
97
- result.set(path, subEntry);
98
- }
99
- }
100
- else {
101
- // File entry
102
- result.set(fullPath, { sha: entry.sha, mode: entry.mode });
103
- }
104
- }
105
- return result;
106
- }
107
- /**
108
- * Compare index entries to HEAD tree entries to detect staged changes.
109
- * @param headEntries - Flattened HEAD tree entries
110
- * @param indexEntries - Index entries with stage=0 (non-conflict)
111
- * @returns Array of changes with status and path
112
- */
113
- function compareIndexToHead(headEntries, indexEntries) {
114
- const changes = [];
115
- const indexMap = new Map();
116
- // Build index map (only stage 0 entries, which are normal entries)
117
- for (const entry of indexEntries) {
118
- if (entry.stage === 0) {
119
- indexMap.set(entry.path, { sha: entry.sha, mode: entry.mode, stage: entry.stage });
120
- }
121
- }
122
- // Check for conflict entries (stage > 0)
123
- const conflictPaths = new Set();
124
- for (const entry of indexEntries) {
125
- if (entry.stage > 0) {
126
- conflictPaths.add(entry.path);
127
- }
128
- }
129
- // Add conflict entries as unmerged
130
- for (const path of conflictPaths) {
131
- changes.push({ status: DiffStatus.UNMERGED, path });
132
- }
133
- // Track added and deleted files for rename detection
134
- const addedFiles = [];
135
- const deletedFiles = [];
136
- // Files in index but not in HEAD = Added (potential rename target)
137
- for (const [path, indexEntry] of indexMap) {
138
- if (conflictPaths.has(path))
139
- continue; // Skip conflicts
140
- const headEntry = headEntries.get(path);
141
- if (!headEntry) {
142
- addedFiles.push({ path, sha: indexEntry.sha, mode: indexEntry.mode });
143
- }
144
- else if (headEntry.sha !== indexEntry.sha) {
145
- // Modified
146
- changes.push({
147
- status: DiffStatus.MODIFIED,
148
- path,
149
- oldMode: headEntry.mode,
150
- newMode: indexEntry.mode,
151
- oldSha: headEntry.sha,
152
- newSha: indexEntry.sha
153
- });
154
- }
155
- else if (headEntry.mode !== indexEntry.mode) {
156
- // Mode changed (e.g., chmod +x)
157
- changes.push({
158
- status: DiffStatus.TYPE_CHANGED,
159
- path,
160
- oldMode: headEntry.mode,
161
- newMode: indexEntry.mode,
162
- oldSha: headEntry.sha,
163
- newSha: indexEntry.sha
164
- });
165
- }
166
- }
167
- // Files in HEAD but not in index = Deleted (potential rename source)
168
- for (const [path, headEntry] of headEntries) {
169
- if (conflictPaths.has(path))
170
- continue; // Skip conflicts
171
- if (!indexMap.has(path)) {
172
- deletedFiles.push({ path, sha: headEntry.sha, mode: headEntry.mode });
173
- }
174
- }
175
- // Detect renames: deleted file with same SHA as added file
176
- const renamedSourcePaths = new Set();
177
- const renamedTargetPaths = new Set();
178
- for (const deleted of deletedFiles) {
179
- // Find an added file with the same SHA (exact content match = rename)
180
- const matchingAdded = addedFiles.find(added => added.sha === deleted.sha && !renamedTargetPaths.has(added.path));
181
- if (matchingAdded) {
182
- // This is a rename
183
- changes.push({
184
- status: DiffStatus.RENAMED,
185
- path: matchingAdded.path,
186
- oldPath: deleted.path,
187
- oldMode: deleted.mode,
188
- newMode: matchingAdded.mode,
189
- oldSha: deleted.sha,
190
- newSha: matchingAdded.sha
191
- });
192
- renamedSourcePaths.add(deleted.path);
193
- renamedTargetPaths.add(matchingAdded.path);
194
- }
195
- }
196
- // Add remaining deleted files (not part of rename)
197
- for (const deleted of deletedFiles) {
198
- if (!renamedSourcePaths.has(deleted.path)) {
199
- changes.push({
200
- status: DiffStatus.DELETED,
201
- path: deleted.path,
202
- oldMode: deleted.mode,
203
- oldSha: deleted.sha
204
- });
205
- }
206
- }
207
- // Add remaining added files (not part of rename)
208
- for (const added of addedFiles) {
209
- if (!renamedTargetPaths.has(added.path)) {
210
- changes.push({
211
- status: DiffStatus.ADDED,
212
- path: added.path,
213
- newMode: added.mode,
214
- newSha: added.sha
215
- });
216
- }
217
- }
218
- return changes;
219
- }
220
- /** Global repository context - set by the application before invoking tools */
221
- let globalRepositoryContext = null;
222
- /**
223
- * Set the global repository context for MCP tools.
224
- *
225
- * @description
226
- * This function sets the global repository context that will be used by all
227
- * MCP git tools. The context provides access to the object store, ref store,
228
- * index, and working directory. This must be called before invoking any tools
229
- * that require repository access.
230
- *
231
- * @param ctx - The repository context to set, or null to clear it
232
- * @returns void
233
- *
234
- * @example
235
- * // Set up context before using tools
236
- * setRepositoryContext({
237
- * objectStore: myObjectStore,
238
- * refStore: myRefStore
239
- * })
240
- *
241
- * // Clear context when done
242
- * setRepositoryContext(null)
243
- */
244
- export function setRepositoryContext(ctx) {
245
- globalRepositoryContext = ctx;
246
- }
247
- /**
248
- * Get the global repository context.
249
- *
250
- * @description
251
- * Returns the currently set repository context, or null if no context has
252
- * been set. Tools use this internally to access repository data.
253
- *
254
- * @returns The current repository context, or null if not set
255
- *
256
- * @example
257
- * const ctx = getRepositoryContext()
258
- * if (ctx) {
259
- * const commit = await ctx.objectStore.getCommit(sha)
260
- * }
261
- */
262
- export function getRepositoryContext() {
263
- return globalRepositoryContext;
264
- }
265
- /**
266
- * Validate a path parameter to prevent command injection.
267
- *
268
- * @description
269
- * Security function that validates file paths to prevent path traversal
270
- * attacks and command injection. Rejects paths containing '..' (parent
271
- * directory traversal), absolute paths starting with '/', and shell
272
- * metacharacters.
273
- *
274
- * @param path - The path to validate
275
- * @returns The validated path (defaults to '.' if undefined)
276
- * @throws {Error} If path contains forbidden characters or traversal patterns
277
- *
278
- * @example
279
- * validatePath('src/file.ts') // Returns 'src/file.ts'
280
- * validatePath(undefined) // Returns '.'
281
- * validatePath('../etc/passwd') // Throws Error
282
- * validatePath('/etc/passwd') // Throws Error
283
- */
284
- function validatePath(path) {
285
- if (!path)
286
- return '.';
287
- // Reject path traversal attempts
288
- if (path.includes('..') || path.startsWith('/') || /[<>|&;$`]/.test(path)) {
289
- throw new Error('Invalid path: contains forbidden characters');
290
- }
291
- return path;
292
- }
293
- /**
294
- * Validate a branch or ref name according to git rules.
295
- *
296
- * @description
297
- * Validates that a branch name conforms to git's naming rules. Branch names
298
- * can contain alphanumeric characters, dots, underscores, forward slashes,
299
- * and hyphens. The '..' sequence is forbidden as it's used for range notation.
300
- *
301
- * @param name - The branch/ref name to validate
302
- * @returns The validated name
303
- * @throws {Error} If name contains invalid characters
304
- *
305
- * @example
306
- * validateBranchName('feature/my-branch') // Returns 'feature/my-branch'
307
- * validateBranchName('v1.0.0') // Returns 'v1.0.0'
308
- * validateBranchName('main..develop') // Throws Error
309
- */
310
- function validateBranchName(name) {
311
- // Git branch name rules
312
- if (!/^[a-zA-Z0-9._\/-]+$/.test(name) || name.includes('..')) {
313
- throw new Error('Invalid branch name');
314
- }
315
- return name;
316
- }
317
- /**
318
- * Validate a commit reference (hash, branch, tag, HEAD, etc.).
319
- *
320
- * @description
321
- * Validates commit references which can be SHA hashes, branch names, tag names,
322
- * HEAD, or relative references like HEAD~3 or HEAD^2. The '..' sequence is
323
- * forbidden to prevent range injection.
324
- *
325
- * @param ref - The commit reference to validate
326
- * @returns The validated reference
327
- * @throws {Error} If reference contains invalid characters
328
- *
329
- * @example
330
- * validateCommitRef('abc123def456') // Returns the SHA
331
- * validateCommitRef('HEAD~3') // Returns 'HEAD~3'
332
- * validateCommitRef('main^2') // Returns 'main^2'
333
- * validateCommitRef('a..b') // Throws Error
334
- */
335
- function validateCommitRef(ref) {
336
- // Allow hex hashes, branch names, tags, HEAD, HEAD~n, HEAD^n, etc.
337
- if (!/^[a-zA-Z0-9._\/-~^]+$/.test(ref) || ref.includes('..')) {
338
- throw new Error('Invalid commit reference');
339
- }
340
- return ref;
341
- }
342
- /**
343
- * Validate a URL for git clone operations.
344
- *
345
- * @description
346
- * Security function that validates URLs to prevent shell injection.
347
- * Rejects URLs containing shell metacharacters that could be used
348
- * for command injection.
349
- *
350
- * @param url - The URL to validate
351
- * @returns The validated URL
352
- * @throws {Error} If URL contains shell injection characters
353
- *
354
- * @example
355
- * validateUrl('https://github.com/user/repo.git') // Returns the URL
356
- * validateUrl('git@github.com:user/repo.git') // Returns the URL
357
- * validateUrl('https://evil.com; rm -rf /') // Throws Error
358
- */
359
- function validateUrl(url) {
360
- // Reject shell injection characters in URLs
361
- if (/[<>|&;$`]/.test(url)) {
362
- throw new Error('Invalid URL: contains forbidden characters');
363
- }
364
- return url;
365
- }
366
- /**
367
- * Validate a remote name.
368
- *
369
- * @description
370
- * Validates that a remote name contains only safe characters.
371
- * Remote names can contain alphanumeric characters, dots, underscores,
372
- * and hyphens.
373
- *
374
- * @param name - The remote name to validate
375
- * @returns The validated name
376
- * @throws {Error} If name contains invalid characters
377
- *
378
- * @example
379
- * validateRemoteName('origin') // Returns 'origin'
380
- * validateRemoteName('my-remote') // Returns 'my-remote'
381
- * validateRemoteName('remote/bad') // Throws Error
382
- */
383
- function validateRemoteName(name) {
384
- if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
385
- throw new Error('Invalid remote name');
386
- }
387
- return name;
388
- }
389
- /**
390
- * Convert DiffStatus enum to human-readable text.
391
- *
392
- * @description
393
- * Maps diff status enum values to their git-style display text
394
- * for use in status and diff output formatting.
395
- *
396
- * @param status - The DiffStatus enum value
397
- * @returns Human-readable status string
398
- *
399
- * @example
400
- * getStatusText(DiffStatus.ADDED) // Returns 'new file'
401
- * getStatusText(DiffStatus.DELETED) // Returns 'deleted'
402
- */
403
- function getStatusText(status) {
404
- switch (status) {
405
- case DiffStatus.ADDED:
406
- return 'new file';
407
- case DiffStatus.DELETED:
408
- return 'deleted';
409
- case DiffStatus.MODIFIED:
410
- return 'modified';
411
- case DiffStatus.RENAMED:
412
- return 'renamed';
413
- case DiffStatus.COPIED:
414
- return 'copied';
415
- case DiffStatus.TYPE_CHANGED:
416
- return 'typechange';
417
- case DiffStatus.UNMERGED:
418
- return 'unmerged';
419
- default:
420
- return 'unknown';
421
- }
422
- }
423
- /**
424
- * Format a commit for log output.
425
- *
426
- * @description
427
- * Formats a commit object into a display string, supporting both
428
- * one-line format (abbreviated SHA + subject) and full format
429
- * (complete commit information with author and date).
430
- *
431
- * @param sha - The full 40-character commit SHA
432
- * @param commit - The parsed commit object
433
- * @param oneline - If true, returns abbreviated single-line format
434
- * @returns Formatted commit string
435
- *
436
- * @example
437
- * // One-line format
438
- * formatCommit('abc123...', commit, true)
439
- * // Returns: 'abc123d Fix bug in parser'
440
- *
441
- * // Full format
442
- * formatCommit('abc123...', commit, false)
443
- * // Returns multi-line commit display
444
- */
445
- function formatCommit(sha, commit, oneline) {
446
- if (oneline) {
447
- const subject = commit.message.split('\n')[0];
448
- return `${sha.slice(0, 7)} ${subject}`;
449
- }
450
- const lines = [];
451
- lines.push(`commit ${sha}`);
452
- lines.push(`Author: ${commit.author.name} <${commit.author.email}>`);
453
- const date = new Date(commit.author.timestamp * 1000);
454
- lines.push(`Date: ${date.toUTCString()}`);
455
- lines.push('');
456
- // Indent the commit message
457
- const messageLines = commit.message.split('\n');
458
- for (const line of messageLines) {
459
- lines.push(` ${line}`);
460
- }
461
- lines.push('');
462
- return lines.join('\n');
463
- }
464
- /**
465
- * Internal registry for custom-registered tools.
466
- * @internal
467
- */
468
- const toolRegistry = new Map();
469
- /**
470
- * Registry of available git tools.
471
- *
472
- * @description
473
- * Array containing all built-in git tool definitions. These tools are
474
- * automatically registered in the tool registry on module load. Each
475
- * tool implements a specific git operation following the MCP specification.
476
- *
477
- * Available tools:
478
- * - git_status: Show repository status
479
- * - git_log: Show commit history
480
- * - git_diff: Show differences between commits
481
- * - git_commit: Create a new commit
482
- * - git_branch: List, create, or delete branches
483
- * - git_checkout: Switch branches or restore files
484
- * - git_push: Upload commits to remote
485
- * - git_pull: Fetch and integrate from remote
486
- * - git_clone: Clone a repository
487
- * - git_init: Initialize a new repository
488
- * - git_add: Stage files for commit
489
- * - git_reset: Reset HEAD to a state
490
- * - git_merge: Merge branches
491
- * - git_rebase: Rebase commits
492
- * - git_stash: Stash changes
493
- * - git_tag: Manage tags
494
- * - git_remote: Manage remotes
495
- * - git_fetch: Fetch from remotes
496
- *
497
- * @example
498
- * // Access git tools array
499
- * import { gitTools } from './tools'
500
- *
501
- * for (const tool of gitTools) {
502
- * console.log(`Tool: ${tool.name} - ${tool.description}`)
503
- * }
504
- */
505
- export const gitTools = [
506
- // git_status tool
507
- {
508
- name: 'git_status',
509
- description: 'Get the current status of a git repository, showing staged, unstaged, and untracked files',
510
- inputSchema: {
511
- type: 'object',
512
- properties: {
513
- path: {
514
- type: 'string',
515
- description: 'Path to the git repository',
516
- },
517
- short: {
518
- type: 'boolean',
519
- description: 'Show short-format output',
520
- },
521
- },
522
- },
523
- handler: async (params) => {
524
- const { short } = params;
525
- const ctx = globalRepositoryContext;
526
- // If no repository context, return mock response for backward compatibility
527
- if (!ctx) {
528
- return {
529
- content: [
530
- {
531
- type: 'text',
532
- text: 'No repository context available. Set repository context with setRepositoryContext().',
533
- },
534
- ],
535
- isError: true,
536
- };
537
- }
538
- try {
539
- // Get current branch
540
- const currentBranch = await getCurrentBranch(ctx.refStore);
541
- // Get HEAD commit SHA
542
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
543
- let headSha = null;
544
- if (headRef) {
545
- headSha = await ctx.refStore.getRef(headRef);
546
- }
547
- else {
548
- headSha = await ctx.refStore.getHead();
549
- }
550
- // Build status output
551
- const lines = [];
552
- if (!short) {
553
- if (currentBranch) {
554
- lines.push(`On branch ${currentBranch}`);
555
- }
556
- else {
557
- lines.push(`HEAD detached at ${headSha?.slice(0, 7) || 'unknown'}`);
558
- }
559
- lines.push('');
560
- }
561
- // Get staged changes (index vs HEAD) using direct comparison
562
- let stagedChanges = [];
563
- let untrackedFiles = [];
564
- let workdirChanges = [];
565
- if (ctx.index) {
566
- const indexEntries = await ctx.index.getEntries();
567
- // Get HEAD tree entries for comparison
568
- let headEntries = new Map();
569
- if (headSha) {
570
- const headCommit = await ctx.objectStore.getCommit(headSha);
571
- if (headCommit) {
572
- headEntries = await flattenTree(ctx.objectStore, headCommit.tree);
573
- }
574
- }
575
- // Compare index to HEAD to find staged changes
576
- stagedChanges = compareIndexToHead(headEntries, indexEntries);
577
- // Check for untracked, modified, and deleted files in workdir
578
- if (ctx.workdir) {
579
- const workdirFiles = await ctx.workdir.getFiles();
580
- const indexMap = new Map(indexEntries.filter(e => e.stage === 0).map(e => [e.path, e]));
581
- const workdirMap = new Map(workdirFiles.map(f => [f.path, f]));
582
- // Check files in workdir
583
- for (const file of workdirFiles) {
584
- const indexEntry = indexMap.get(file.path);
585
- if (!indexEntry) {
586
- // File in workdir but not in index = untracked
587
- untrackedFiles.push(file.path);
588
- }
589
- else if (indexEntry.sha !== file.sha) {
590
- // File content differs from index = unstaged content modification
591
- workdirChanges.push({ status: DiffStatus.MODIFIED, path: file.path });
592
- }
593
- else if (indexEntry.mode !== file.mode) {
594
- // Same content but different mode = unstaged mode change
595
- workdirChanges.push({ status: DiffStatus.TYPE_CHANGED, path: file.path });
596
- }
597
- }
598
- // Check for deleted files (in index but not in workdir)
599
- for (const [path, _indexEntry] of indexMap) {
600
- if (!workdirMap.has(path)) {
601
- // File in index but not in workdir = unstaged deletion
602
- workdirChanges.push({ status: DiffStatus.DELETED, path });
603
- }
604
- }
605
- }
606
- }
607
- // Separate unmerged (conflict) entries
608
- const unmergedChanges = stagedChanges.filter(c => c.status === DiffStatus.UNMERGED);
609
- const normalStagedChanges = stagedChanges.filter(c => c.status !== DiffStatus.UNMERGED);
610
- // Format unmerged (conflict) files
611
- if (unmergedChanges.length > 0) {
612
- if (!short) {
613
- lines.push('Unmerged paths:');
614
- lines.push(' (use "git add <file>..." to mark resolution)');
615
- lines.push('');
616
- }
617
- for (const entry of unmergedChanges) {
618
- if (short) {
619
- lines.push(`UU ${entry.path}`);
620
- }
621
- else {
622
- lines.push(` both modified: ${entry.path}`);
623
- }
624
- }
625
- if (!short)
626
- lines.push('');
627
- }
628
- // Format staged changes
629
- if (normalStagedChanges.length > 0) {
630
- if (!short) {
631
- lines.push('Changes to be committed:');
632
- lines.push(' (use "git restore --staged <file>..." to unstage)');
633
- lines.push('');
634
- }
635
- for (const entry of normalStagedChanges) {
636
- if (short) {
637
- // XY format: X = index status, Y = workdir status (space = no change)
638
- const workdirStatus = workdirChanges.find(w => w.path === entry.path) ? 'M' : ' ';
639
- if (entry.status === DiffStatus.RENAMED && entry.oldPath) {
640
- lines.push(`${entry.status}${workdirStatus} ${entry.oldPath} -> ${entry.path}`);
641
- }
642
- else {
643
- lines.push(`${entry.status}${workdirStatus} ${entry.path}`);
644
- }
645
- }
646
- else {
647
- const statusText = getStatusText(entry.status);
648
- if (entry.status === DiffStatus.RENAMED && entry.oldPath) {
649
- lines.push(` ${statusText}: ${entry.oldPath} -> ${entry.path}`);
650
- }
651
- else {
652
- lines.push(` ${statusText}: ${entry.path}`);
653
- }
654
- }
655
- }
656
- if (!short)
657
- lines.push('');
658
- }
659
- // Format unstaged workdir changes (not already counted as staged)
660
- const pureWorkdirChanges = workdirChanges.filter(w => !normalStagedChanges.find(s => s.path === w.path));
661
- if (pureWorkdirChanges.length > 0) {
662
- if (!short) {
663
- lines.push('Changes not staged for commit:');
664
- lines.push(' (use "git add <file>..." to update what will be committed)');
665
- lines.push('');
666
- }
667
- for (const entry of pureWorkdirChanges) {
668
- if (short) {
669
- lines.push(` ${entry.status} ${entry.path}`);
670
- }
671
- else {
672
- const statusText = getStatusText(entry.status);
673
- lines.push(` ${statusText}: ${entry.path}`);
674
- }
675
- }
676
- if (!short)
677
- lines.push('');
678
- }
679
- // Format untracked files
680
- if (untrackedFiles.length > 0) {
681
- if (!short) {
682
- lines.push('Untracked files:');
683
- lines.push(' (use "git add <file>..." to include in what will be committed)');
684
- lines.push('');
685
- }
686
- for (const file of untrackedFiles) {
687
- if (short) {
688
- lines.push(`?? ${file}`);
689
- }
690
- else {
691
- lines.push(` ${file}`);
692
- }
693
- }
694
- if (!short)
695
- lines.push('');
696
- }
697
- // If no changes at all
698
- if (normalStagedChanges.length === 0 && workdirChanges.length === 0 &&
699
- untrackedFiles.length === 0 && unmergedChanges.length === 0) {
700
- if (!short) {
701
- lines.push('nothing to commit, working tree clean');
702
- }
703
- }
704
- return {
705
- content: [
706
- {
707
- type: 'text',
708
- text: lines.join('\n'),
709
- },
710
- ],
711
- isError: false,
712
- };
713
- }
714
- catch (error) {
715
- return {
716
- content: [
717
- {
718
- type: 'text',
719
- text: `Error getting status: ${error instanceof Error ? error.message : String(error)}`,
720
- },
721
- ],
722
- isError: true,
723
- };
724
- }
725
- },
726
- },
727
- // git_log tool
728
- {
729
- name: 'git_log',
730
- description: 'Show the commit log history for a git repository',
731
- inputSchema: {
732
- type: 'object',
733
- properties: {
734
- path: {
735
- type: 'string',
736
- description: 'Path to the git repository',
737
- },
738
- maxCount: {
739
- type: 'number',
740
- description: 'Maximum number of commits to show',
741
- minimum: 1,
742
- },
743
- oneline: {
744
- type: 'boolean',
745
- description: 'Show each commit on a single line',
746
- },
747
- ref: {
748
- type: 'string',
749
- description: 'Branch, tag, or commit reference to show log for',
750
- },
751
- },
752
- },
753
- handler: async (params) => {
754
- const { maxCount, oneline, ref } = params;
755
- const ctx = globalRepositoryContext;
756
- // If no repository context, return error
757
- if (!ctx) {
758
- return {
759
- content: [
760
- {
761
- type: 'text',
762
- text: 'No repository context available. Set repository context with setRepositoryContext().',
763
- },
764
- ],
765
- isError: true,
766
- };
767
- }
768
- try {
769
- // Resolve starting commit
770
- let startSha = null;
771
- if (ref) {
772
- // Validate and resolve ref
773
- const validatedRef = validateCommitRef(ref);
774
- // Try as branch first
775
- startSha = await ctx.refStore.getRef(`refs/heads/${validatedRef}`);
776
- // Try as direct SHA if not found
777
- if (!startSha && /^[a-f0-9]{40}$/i.test(validatedRef)) {
778
- startSha = validatedRef;
779
- }
780
- // Try as tag
781
- if (!startSha) {
782
- startSha = await ctx.refStore.getRef(`refs/tags/${validatedRef}`);
783
- }
784
- }
785
- else {
786
- // Use HEAD
787
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
788
- if (headRef) {
789
- startSha = await ctx.refStore.getRef(headRef);
790
- }
791
- else {
792
- startSha = await ctx.refStore.getHead();
793
- }
794
- }
795
- if (!startSha) {
796
- return {
797
- content: [
798
- {
799
- type: 'text',
800
- text: ref ? `fatal: bad revision '${ref}'` : 'fatal: HEAD not found',
801
- },
802
- ],
803
- isError: true,
804
- };
805
- }
806
- // Create commit provider adapter
807
- const commitProvider = {
808
- getCommit: async (sha) => ctx.objectStore.getCommit(sha)
809
- };
810
- // Walk commits
811
- const traversalOptions = {
812
- maxCount: maxCount,
813
- sort: 'date'
814
- };
815
- const commits = [];
816
- for await (const traversalCommit of walkCommits(commitProvider, startSha, traversalOptions)) {
817
- commits.push(formatCommit(traversalCommit.sha, traversalCommit.commit, oneline || false));
818
- }
819
- const output = commits.join(oneline ? '\n' : '');
820
- return {
821
- content: [
822
- {
823
- type: 'text',
824
- text: output || 'No commits found',
825
- },
826
- ],
827
- };
828
- }
829
- catch (error) {
830
- return {
831
- content: [
832
- {
833
- type: 'text',
834
- text: `Error getting log: ${error instanceof Error ? error.message : String(error)}`,
835
- },
836
- ],
837
- isError: true,
838
- };
839
- }
840
- },
841
- },
842
- // git_diff tool
843
- {
844
- name: 'git_diff',
845
- description: 'Show differences between commits, commit and working tree',
846
- inputSchema: {
847
- type: 'object',
848
- properties: {
849
- path: {
850
- type: 'string',
851
- description: 'Path to the git repository',
852
- },
853
- staged: {
854
- type: 'boolean',
855
- description: 'Show staged changes (--cached)',
856
- },
857
- commit1: {
858
- type: 'string',
859
- description: 'First commit to compare',
860
- },
861
- commit2: {
862
- type: 'string',
863
- description: 'Second commit to compare',
864
- },
865
- },
866
- },
867
- handler: async (params) => {
868
- const { staged, commit1, commit2 } = params;
869
- const ctx = globalRepositoryContext;
870
- // If no repository context, return error
871
- if (!ctx) {
872
- return {
873
- content: [
874
- {
875
- type: 'text',
876
- text: 'No repository context available. Set repository context with setRepositoryContext().',
877
- },
878
- ],
879
- isError: true,
880
- };
881
- }
882
- try {
883
- // Create diff store adapter
884
- const diffStore = {
885
- getTree: (sha) => ctx.objectStore.getTree(sha),
886
- getBlob: (sha) => ctx.objectStore.getBlob(sha),
887
- exists: (sha) => ctx.objectStore.hasObject(sha)
888
- };
889
- let oldTreeSha = null;
890
- let newTreeSha = null;
891
- // Resolve commits to tree SHAs
892
- const resolveCommitToTree = async (commitRef) => {
893
- // Validate ref
894
- const validatedRef = validateCommitRef(commitRef);
895
- // Try as direct SHA
896
- if (/^[a-f0-9]{40}$/i.test(validatedRef)) {
897
- const commit = await ctx.objectStore.getCommit(validatedRef);
898
- return commit?.tree || null;
899
- }
900
- // Try as branch
901
- let sha = await ctx.refStore.getRef(`refs/heads/${validatedRef}`);
902
- if (!sha) {
903
- sha = await ctx.refStore.getRef(`refs/tags/${validatedRef}`);
904
- }
905
- if (sha) {
906
- const commit = await ctx.objectStore.getCommit(sha);
907
- return commit?.tree || null;
908
- }
909
- return null;
910
- };
911
- if (commit1 && commit2) {
912
- // Compare two commits
913
- oldTreeSha = await resolveCommitToTree(commit1);
914
- newTreeSha = await resolveCommitToTree(commit2);
915
- }
916
- else if (commit1) {
917
- // Compare commit to HEAD
918
- oldTreeSha = await resolveCommitToTree(commit1);
919
- // Get HEAD tree
920
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
921
- let headSha = null;
922
- if (headRef) {
923
- headSha = await ctx.refStore.getRef(headRef);
924
- }
925
- else {
926
- headSha = await ctx.refStore.getHead();
927
- }
928
- if (headSha) {
929
- const headCommit = await ctx.objectStore.getCommit(headSha);
930
- newTreeSha = headCommit?.tree || null;
931
- }
932
- }
933
- else if (staged) {
934
- // Compare HEAD to index (staged changes)
935
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
936
- let headSha = null;
937
- if (headRef) {
938
- headSha = await ctx.refStore.getRef(headRef);
939
- }
940
- else {
941
- headSha = await ctx.refStore.getHead();
942
- }
943
- if (headSha) {
944
- const headCommit = await ctx.objectStore.getCommit(headSha);
945
- oldTreeSha = headCommit?.tree || null;
946
- }
947
- // For staged diff, we would compare against index
948
- // newTreeSha would be built from index entries
949
- newTreeSha = null; // Index comparison not fully implemented
950
- }
951
- else {
952
- // Default: compare working tree to index (unstaged changes)
953
- // This requires working directory support
954
- return {
955
- content: [
956
- {
957
- type: 'text',
958
- text: 'Working tree diff requires workdir context (not yet implemented)',
959
- },
960
- ],
961
- };
962
- }
963
- if (oldTreeSha === null && newTreeSha === null) {
964
- return {
965
- content: [
966
- {
967
- type: 'text',
968
- text: 'No changes to display',
969
- },
970
- ],
971
- };
972
- }
973
- // Perform the diff
974
- const diffResult = await diffTrees(diffStore, oldTreeSha, newTreeSha, {
975
- recursive: true,
976
- detectRenames: true
977
- });
978
- // Format diff output
979
- const lines = [];
980
- for (const entry of diffResult.entries) {
981
- lines.push(`diff --git a/${entry.oldPath || entry.path} b/${entry.path}`);
982
- if (entry.status === DiffStatus.ADDED) {
983
- lines.push('new file mode ' + entry.newMode);
984
- }
985
- else if (entry.status === DiffStatus.DELETED) {
986
- lines.push('deleted file mode ' + entry.oldMode);
987
- }
988
- else if (entry.status === DiffStatus.RENAMED) {
989
- lines.push(`rename from ${entry.oldPath}`);
990
- lines.push(`rename to ${entry.path}`);
991
- if (entry.similarity !== undefined) {
992
- lines.push(`similarity index ${entry.similarity}%`);
993
- }
994
- }
995
- lines.push(`index ${entry.oldSha?.slice(0, 7) || '0000000'}..${entry.newSha?.slice(0, 7) || '0000000'}`);
996
- lines.push(`--- ${entry.status === DiffStatus.ADDED ? '/dev/null' : 'a/' + (entry.oldPath || entry.path)}`);
997
- lines.push(`+++ ${entry.status === DiffStatus.DELETED ? '/dev/null' : 'b/' + entry.path}`);
998
- lines.push(''); // Placeholder for actual content diff
999
- }
1000
- // Add stats summary
1001
- lines.push('');
1002
- lines.push(`${diffResult.entries.length} file(s) changed`);
1003
- return {
1004
- content: [
1005
- {
1006
- type: 'text',
1007
- text: lines.join('\n') || 'No changes',
1008
- },
1009
- ],
1010
- };
1011
- }
1012
- catch (error) {
1013
- return {
1014
- content: [
1015
- {
1016
- type: 'text',
1017
- text: `Error getting diff: ${error instanceof Error ? error.message : String(error)}`,
1018
- },
1019
- ],
1020
- isError: true,
1021
- };
1022
- }
1023
- },
1024
- },
1025
- // git_commit tool
1026
- {
1027
- name: 'git_commit',
1028
- description: 'Create a new commit with the staged changes in the repository',
1029
- inputSchema: {
1030
- type: 'object',
1031
- properties: {
1032
- path: {
1033
- type: 'string',
1034
- description: 'Path to the git repository',
1035
- },
1036
- message: {
1037
- type: 'string',
1038
- description: 'Commit message',
1039
- },
1040
- author: {
1041
- type: 'string',
1042
- description: 'Author name for the commit',
1043
- },
1044
- email: {
1045
- type: 'string',
1046
- description: 'Author email for the commit',
1047
- },
1048
- amend: {
1049
- type: 'boolean',
1050
- description: 'Amend the previous commit',
1051
- },
1052
- },
1053
- required: ['message'],
1054
- },
1055
- handler: async (params) => {
1056
- const { message, author, email, amend } = params;
1057
- const ctx = globalRepositoryContext;
1058
- // If no repository context, return error
1059
- if (!ctx) {
1060
- return {
1061
- content: [
1062
- {
1063
- type: 'text',
1064
- text: 'No repository context available. Set repository context with setRepositoryContext().',
1065
- },
1066
- ],
1067
- isError: true,
1068
- };
1069
- }
1070
- // Sanitize message - reject shell injection characters (for backward compat)
1071
- if (/[`$]/.test(message)) {
1072
- throw new Error('Invalid commit message: contains forbidden characters');
1073
- }
1074
- // Validate author and email if provided
1075
- if (author && email) {
1076
- if (/[<>"`$\\]/.test(author) || /[<>"`$\\]/.test(email)) {
1077
- throw new Error('Invalid author/email: contains forbidden characters');
1078
- }
1079
- }
1080
- try {
1081
- // Get current HEAD
1082
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
1083
- let parentSha = null;
1084
- if (headRef) {
1085
- parentSha = await ctx.refStore.getRef(headRef);
1086
- }
1087
- else {
1088
- parentSha = await ctx.refStore.getHead();
1089
- }
1090
- // For a real commit, we need:
1091
- // 1. A tree SHA from the index
1092
- // 2. Parent commit(s)
1093
- // 3. Author/committer info
1094
- // If we don't have an index, we can't create a real commit
1095
- if (!ctx.index) {
1096
- return {
1097
- content: [
1098
- {
1099
- type: 'text',
1100
- text: 'Cannot create commit: no index/staging area available',
1101
- },
1102
- ],
1103
- isError: true,
1104
- };
1105
- }
1106
- // Get index entries and build tree
1107
- // For now, we need a tree SHA - in a full implementation we'd build it from index
1108
- // This is a simplified version that requires the tree to already exist
1109
- const now = Math.floor(Date.now() / 1000);
1110
- const timezone = '+0000'; // UTC for simplicity
1111
- const commitAuthor = {
1112
- name: author || 'Unknown',
1113
- email: email || 'unknown@example.com',
1114
- timestamp: now,
1115
- timezone
1116
- };
1117
- // For amend, get the parent's tree (simplified)
1118
- let treeSha = null;
1119
- const parents = [];
1120
- if (amend && parentSha) {
1121
- // Get parent commit for amend
1122
- const parentCommit = await ctx.objectStore.getCommit(parentSha);
1123
- if (parentCommit) {
1124
- treeSha = parentCommit.tree;
1125
- parents.push(...parentCommit.parents);
1126
- }
1127
- }
1128
- else if (parentSha) {
1129
- // Regular commit - parent is current HEAD
1130
- const parentCommit = await ctx.objectStore.getCommit(parentSha);
1131
- if (parentCommit) {
1132
- treeSha = parentCommit.tree; // Use parent's tree for now (no changes)
1133
- }
1134
- parents.push(parentSha);
1135
- }
1136
- if (!treeSha) {
1137
- return {
1138
- content: [
1139
- {
1140
- type: 'text',
1141
- text: 'Cannot create commit: unable to determine tree SHA',
1142
- },
1143
- ],
1144
- isError: true,
1145
- };
1146
- }
1147
- // Create the commit using gitdo's commit creation
1148
- const commitOptions = {
1149
- message,
1150
- tree: treeSha,
1151
- parents,
1152
- author: commitAuthor,
1153
- committer: commitAuthor,
1154
- allowEmpty: true
1155
- };
1156
- // Create object store adapter for createCommit
1157
- const commitStore = {
1158
- getObject: ctx.objectStore.getObject,
1159
- storeObject: ctx.objectStore.storeObject,
1160
- hasObject: ctx.objectStore.hasObject
1161
- };
1162
- const result = await createCommit(commitStore, commitOptions);
1163
- // Update the ref to point to the new commit
1164
- if (headRef) {
1165
- await ctx.refStore.setRef(headRef, result.sha);
1166
- }
1167
- return {
1168
- content: [
1169
- {
1170
- type: 'text',
1171
- text: `[${headRef ? headRef.replace('refs/heads/', '') : 'detached HEAD'} ${result.sha.slice(0, 7)}] ${message.split('\n')[0]}`,
1172
- },
1173
- ],
1174
- };
1175
- }
1176
- catch (error) {
1177
- return {
1178
- content: [
1179
- {
1180
- type: 'text',
1181
- text: `Error creating commit: ${error instanceof Error ? error.message : String(error)}`,
1182
- },
1183
- ],
1184
- isError: true,
1185
- };
1186
- }
1187
- },
1188
- },
1189
- // git_branch tool
1190
- {
1191
- name: 'git_branch',
1192
- description: 'List, create, or delete branches in the repository',
1193
- inputSchema: {
1194
- type: 'object',
1195
- properties: {
1196
- path: {
1197
- type: 'string',
1198
- description: 'Path to the git repository',
1199
- },
1200
- list: {
1201
- type: 'boolean',
1202
- description: 'List branches',
1203
- },
1204
- name: {
1205
- type: 'string',
1206
- description: 'Name of the branch to create or delete',
1207
- },
1208
- delete: {
1209
- type: 'boolean',
1210
- description: 'Delete the specified branch',
1211
- },
1212
- all: {
1213
- type: 'boolean',
1214
- description: 'List all branches including remote branches',
1215
- },
1216
- },
1217
- },
1218
- handler: async (params) => {
1219
- const { list, name, delete: del, all } = params;
1220
- const ctx = globalRepositoryContext;
1221
- // If no repository context, return error
1222
- if (!ctx) {
1223
- return {
1224
- content: [
1225
- {
1226
- type: 'text',
1227
- text: 'No repository context available. Set repository context with setRepositoryContext().',
1228
- },
1229
- ],
1230
- isError: true,
1231
- };
1232
- }
1233
- try {
1234
- // List branches
1235
- if (list || (!name && !del)) {
1236
- const branches = await listBranches(ctx.refStore, {
1237
- all: all || false,
1238
- remote: false
1239
- });
1240
- if (branches.length === 0) {
1241
- return {
1242
- content: [
1243
- {
1244
- type: 'text',
1245
- text: 'No branches found',
1246
- },
1247
- ],
1248
- };
1249
- }
1250
- const lines = [];
1251
- for (const branch of branches) {
1252
- const prefix = branch.current ? '* ' : ' ';
1253
- lines.push(`${prefix}${branch.name}`);
1254
- }
1255
- return {
1256
- content: [
1257
- {
1258
- type: 'text',
1259
- text: lines.join('\n'),
1260
- },
1261
- ],
1262
- };
1263
- }
1264
- // Delete branch
1265
- if (del && name) {
1266
- const validatedName = validateBranchName(name);
1267
- const result = await deleteBranch(ctx.refStore, { name: validatedName });
1268
- return {
1269
- content: [
1270
- {
1271
- type: 'text',
1272
- text: `Deleted branch ${validatedName} (was ${result.sha.slice(0, 7)}).`,
1273
- },
1274
- ],
1275
- };
1276
- }
1277
- // Create branch
1278
- if (name) {
1279
- const validatedName = validateBranchName(name);
1280
- const result = await createBranch(ctx.refStore, { name: validatedName });
1281
- return {
1282
- content: [
1283
- {
1284
- type: 'text',
1285
- text: result.created
1286
- ? `Created branch '${validatedName}' at ${result.sha.slice(0, 7)}`
1287
- : `Branch '${validatedName}' already exists at ${result.sha.slice(0, 7)}`,
1288
- },
1289
- ],
1290
- };
1291
- }
1292
- return {
1293
- content: [
1294
- {
1295
- type: 'text',
1296
- text: 'No branch operation specified',
1297
- },
1298
- ],
1299
- };
1300
- }
1301
- catch (error) {
1302
- return {
1303
- content: [
1304
- {
1305
- type: 'text',
1306
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1307
- },
1308
- ],
1309
- isError: true,
1310
- };
1311
- }
1312
- },
1313
- },
1314
- // git_checkout tool
1315
- {
1316
- name: 'git_checkout',
1317
- description: 'Switch branches or restore working tree files using git checkout',
1318
- inputSchema: {
1319
- type: 'object',
1320
- properties: {
1321
- path: {
1322
- type: 'string',
1323
- description: 'Path to the git repository',
1324
- },
1325
- ref: {
1326
- type: 'string',
1327
- description: 'Branch, tag, or commit to checkout',
1328
- },
1329
- createBranch: {
1330
- type: 'boolean',
1331
- description: 'Create a new branch with the given ref name',
1332
- },
1333
- },
1334
- required: ['ref'],
1335
- },
1336
- handler: async (params) => {
1337
- const { path, ref, createBranch } = params;
1338
- const validatedPath = validatePath(path);
1339
- const validatedRef = validateBranchName(ref);
1340
- const args = ['checkout'];
1341
- if (createBranch)
1342
- args.push('-b');
1343
- args.push(validatedRef);
1344
- return {
1345
- content: [
1346
- {
1347
- type: 'text',
1348
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1349
- },
1350
- ],
1351
- };
1352
- },
1353
- },
1354
- // git_push tool
1355
- {
1356
- name: 'git_push',
1357
- description: 'Upload local commits to a remote repository using git push',
1358
- inputSchema: {
1359
- type: 'object',
1360
- properties: {
1361
- path: {
1362
- type: 'string',
1363
- description: 'Path to the git repository',
1364
- },
1365
- remote: {
1366
- type: 'string',
1367
- description: 'Name of the remote (e.g., origin)',
1368
- },
1369
- branch: {
1370
- type: 'string',
1371
- description: 'Branch to push',
1372
- },
1373
- force: {
1374
- type: 'boolean',
1375
- description: 'Force push (use with caution)',
1376
- },
1377
- setUpstream: {
1378
- type: 'boolean',
1379
- description: 'Set upstream for the current branch',
1380
- },
1381
- },
1382
- },
1383
- handler: async (params) => {
1384
- const { path, remote, branch, force, setUpstream } = params;
1385
- const validatedPath = validatePath(path);
1386
- const args = ['push'];
1387
- if (force)
1388
- args.push('--force');
1389
- if (setUpstream)
1390
- args.push('-u');
1391
- if (remote)
1392
- args.push(validateRemoteName(remote));
1393
- if (branch)
1394
- args.push(validateBranchName(branch));
1395
- return {
1396
- content: [
1397
- {
1398
- type: 'text',
1399
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1400
- },
1401
- ],
1402
- };
1403
- },
1404
- },
1405
- // git_pull tool
1406
- {
1407
- name: 'git_pull',
1408
- description: 'Fetch and integrate changes from a remote repository using git pull',
1409
- inputSchema: {
1410
- type: 'object',
1411
- properties: {
1412
- path: {
1413
- type: 'string',
1414
- description: 'Path to the git repository',
1415
- },
1416
- remote: {
1417
- type: 'string',
1418
- description: 'Name of the remote (e.g., origin)',
1419
- },
1420
- branch: {
1421
- type: 'string',
1422
- description: 'Branch to pull',
1423
- },
1424
- rebase: {
1425
- type: 'boolean',
1426
- description: 'Rebase instead of merge',
1427
- },
1428
- },
1429
- },
1430
- handler: async (params) => {
1431
- const { path, remote, branch, rebase } = params;
1432
- const validatedPath = validatePath(path);
1433
- const args = ['pull'];
1434
- if (rebase)
1435
- args.push('--rebase');
1436
- if (remote)
1437
- args.push(validateRemoteName(remote));
1438
- if (branch)
1439
- args.push(validateBranchName(branch));
1440
- return {
1441
- content: [
1442
- {
1443
- type: 'text',
1444
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1445
- },
1446
- ],
1447
- };
1448
- },
1449
- },
1450
- // git_clone tool
1451
- {
1452
- name: 'git_clone',
1453
- description: 'Copy a repository from a remote URL to a local directory using git clone',
1454
- inputSchema: {
1455
- type: 'object',
1456
- properties: {
1457
- url: {
1458
- type: 'string',
1459
- description: 'URL of the repository to clone',
1460
- },
1461
- destination: {
1462
- type: 'string',
1463
- description: 'Local path to clone into',
1464
- },
1465
- depth: {
1466
- type: 'number',
1467
- description: 'Create a shallow clone with specified depth',
1468
- },
1469
- branch: {
1470
- type: 'string',
1471
- description: 'Branch to clone',
1472
- },
1473
- bare: {
1474
- type: 'boolean',
1475
- description: 'Create a bare repository',
1476
- },
1477
- },
1478
- required: ['url'],
1479
- },
1480
- handler: async (params) => {
1481
- const { url, destination, depth, branch, bare } = params;
1482
- const validatedUrl = validateUrl(url);
1483
- const args = ['clone'];
1484
- if (depth)
1485
- args.push(`--depth=${depth}`);
1486
- if (branch)
1487
- args.push(`--branch=${validateBranchName(branch)}`);
1488
- if (bare)
1489
- args.push('--bare');
1490
- args.push(validatedUrl);
1491
- if (destination)
1492
- args.push(validatePath(destination));
1493
- return {
1494
- content: [
1495
- {
1496
- type: 'text',
1497
- text: `Executed: git ${args.join(' ')}`,
1498
- },
1499
- ],
1500
- };
1501
- },
1502
- },
1503
- // git_init tool
1504
- {
1505
- name: 'git_init',
1506
- description: 'Create an empty git repository or reinitialize an existing one',
1507
- inputSchema: {
1508
- type: 'object',
1509
- properties: {
1510
- path: {
1511
- type: 'string',
1512
- description: 'Path where the repository should be initialized',
1513
- },
1514
- bare: {
1515
- type: 'boolean',
1516
- description: 'Create a bare repository',
1517
- },
1518
- initialBranch: {
1519
- type: 'string',
1520
- description: 'Name for the initial branch',
1521
- },
1522
- },
1523
- required: ['path'],
1524
- },
1525
- handler: async (params) => {
1526
- const { path, bare, initialBranch } = params;
1527
- const validatedPath = validatePath(path);
1528
- const args = ['init'];
1529
- if (bare)
1530
- args.push('--bare');
1531
- if (initialBranch)
1532
- args.push(`--initial-branch=${validateBranchName(initialBranch)}`);
1533
- args.push(validatedPath);
1534
- return {
1535
- content: [
1536
- {
1537
- type: 'text',
1538
- text: `Executed: git ${args.join(' ')}`,
1539
- },
1540
- ],
1541
- };
1542
- },
1543
- },
1544
- // git_add tool
1545
- {
1546
- name: 'git_add',
1547
- description: 'Add file contents to the staging area for the next commit',
1548
- inputSchema: {
1549
- type: 'object',
1550
- properties: {
1551
- path: {
1552
- type: 'string',
1553
- description: 'Path to the git repository',
1554
- },
1555
- files: {
1556
- type: 'array',
1557
- items: { type: 'string' },
1558
- description: 'List of files to add',
1559
- },
1560
- all: {
1561
- type: 'boolean',
1562
- description: 'Add all changes in the working tree',
1563
- },
1564
- force: {
1565
- type: 'boolean',
1566
- description: 'Allow adding otherwise ignored files',
1567
- },
1568
- },
1569
- },
1570
- handler: async (params) => {
1571
- const { path, files, all, force } = params;
1572
- const validatedPath = validatePath(path);
1573
- const args = ['add'];
1574
- if (all)
1575
- args.push('--all');
1576
- if (force)
1577
- args.push('--force');
1578
- if (files) {
1579
- // Validate each file path
1580
- const validatedFiles = files.map((f) => {
1581
- if (/[<>|&;$`]/.test(f)) {
1582
- throw new Error('Invalid file path: contains forbidden characters');
1583
- }
1584
- return f;
1585
- });
1586
- args.push(...validatedFiles);
1587
- }
1588
- return {
1589
- content: [
1590
- {
1591
- type: 'text',
1592
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1593
- },
1594
- ],
1595
- };
1596
- },
1597
- },
1598
- // git_reset tool
1599
- {
1600
- name: 'git_reset',
1601
- description: 'Reset current HEAD to a specified state',
1602
- inputSchema: {
1603
- type: 'object',
1604
- properties: {
1605
- path: {
1606
- type: 'string',
1607
- description: 'Path to the git repository',
1608
- },
1609
- mode: {
1610
- type: 'string',
1611
- enum: ['soft', 'mixed', 'hard'],
1612
- description: 'Reset mode: soft, mixed, or hard',
1613
- },
1614
- commit: {
1615
- type: 'string',
1616
- description: 'Commit to reset to',
1617
- },
1618
- },
1619
- },
1620
- handler: async (params) => {
1621
- const { path, mode, commit } = params;
1622
- const validatedPath = validatePath(path);
1623
- const args = ['reset'];
1624
- if (mode)
1625
- args.push(`--${mode}`);
1626
- if (commit)
1627
- args.push(validateCommitRef(commit));
1628
- return {
1629
- content: [
1630
- {
1631
- type: 'text',
1632
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1633
- },
1634
- ],
1635
- };
1636
- },
1637
- },
1638
- // git_merge tool
1639
- {
1640
- name: 'git_merge',
1641
- description: 'Merge one or more branches into the current branch',
1642
- inputSchema: {
1643
- type: 'object',
1644
- properties: {
1645
- path: {
1646
- type: 'string',
1647
- description: 'Path to the git repository',
1648
- },
1649
- branch: {
1650
- type: 'string',
1651
- description: 'Branch to merge into current branch',
1652
- },
1653
- noFf: {
1654
- type: 'boolean',
1655
- description: 'Create a merge commit even when fast-forward is possible',
1656
- },
1657
- squash: {
1658
- type: 'boolean',
1659
- description: 'Squash commits into a single commit',
1660
- },
1661
- },
1662
- required: ['branch'],
1663
- },
1664
- handler: async (params) => {
1665
- const { path, branch, noFf, squash } = params;
1666
- const validatedPath = validatePath(path);
1667
- const args = ['merge'];
1668
- if (noFf)
1669
- args.push('--no-ff');
1670
- if (squash)
1671
- args.push('--squash');
1672
- args.push(validateBranchName(branch));
1673
- return {
1674
- content: [
1675
- {
1676
- type: 'text',
1677
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1678
- },
1679
- ],
1680
- };
1681
- },
1682
- },
1683
- // git_rebase tool
1684
- {
1685
- name: 'git_rebase',
1686
- description: 'Reapply commits on top of another base tip',
1687
- inputSchema: {
1688
- type: 'object',
1689
- properties: {
1690
- path: {
1691
- type: 'string',
1692
- description: 'Path to the git repository',
1693
- },
1694
- onto: {
1695
- type: 'string',
1696
- description: 'Branch or commit to rebase onto',
1697
- },
1698
- abort: {
1699
- type: 'boolean',
1700
- description: 'Abort an in-progress rebase',
1701
- },
1702
- continue: {
1703
- type: 'boolean',
1704
- description: 'Continue an in-progress rebase',
1705
- },
1706
- },
1707
- },
1708
- handler: async (params) => {
1709
- const { path, onto, abort, continue: cont } = params;
1710
- const validatedPath = validatePath(path);
1711
- const args = ['rebase'];
1712
- if (abort)
1713
- args.push('--abort');
1714
- else if (cont)
1715
- args.push('--continue');
1716
- else if (onto)
1717
- args.push(validateCommitRef(onto));
1718
- return {
1719
- content: [
1720
- {
1721
- type: 'text',
1722
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1723
- },
1724
- ],
1725
- };
1726
- },
1727
- },
1728
- // git_stash tool
1729
- {
1730
- name: 'git_stash',
1731
- description: 'Stash the changes in a dirty working directory away',
1732
- inputSchema: {
1733
- type: 'object',
1734
- properties: {
1735
- path: {
1736
- type: 'string',
1737
- description: 'Path to the git repository',
1738
- },
1739
- action: {
1740
- type: 'string',
1741
- enum: ['push', 'pop', 'list', 'drop', 'apply', 'clear'],
1742
- description: 'Stash action to perform',
1743
- },
1744
- message: {
1745
- type: 'string',
1746
- description: 'Message for the stash entry',
1747
- },
1748
- },
1749
- },
1750
- handler: async (params) => {
1751
- const { path, action, message } = params;
1752
- const validatedPath = validatePath(path);
1753
- const args = ['stash'];
1754
- if (action)
1755
- args.push(action);
1756
- if (message && action === 'push') {
1757
- // Validate stash message for shell injection
1758
- if (/[`$]/.test(message)) {
1759
- throw new Error('Invalid stash message: contains forbidden characters');
1760
- }
1761
- args.push('-m', message);
1762
- }
1763
- return {
1764
- content: [
1765
- {
1766
- type: 'text',
1767
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1768
- },
1769
- ],
1770
- };
1771
- },
1772
- },
1773
- // git_tag tool
1774
- {
1775
- name: 'git_tag',
1776
- description: 'Create, list, delete, or verify tags in the repository',
1777
- inputSchema: {
1778
- type: 'object',
1779
- properties: {
1780
- path: {
1781
- type: 'string',
1782
- description: 'Path to the git repository',
1783
- },
1784
- name: {
1785
- type: 'string',
1786
- description: 'Name of the tag',
1787
- },
1788
- message: {
1789
- type: 'string',
1790
- description: 'Message for annotated tag',
1791
- },
1792
- delete: {
1793
- type: 'boolean',
1794
- description: 'Delete the specified tag',
1795
- },
1796
- },
1797
- },
1798
- handler: async (params) => {
1799
- const { path, name, message, delete: del } = params;
1800
- const validatedPath = validatePath(path);
1801
- const args = ['tag'];
1802
- if (del && name)
1803
- args.push('-d', validateBranchName(name));
1804
- else if (message && name) {
1805
- // Validate tag message for shell injection
1806
- if (/[`$]/.test(message)) {
1807
- throw new Error('Invalid tag message: contains forbidden characters');
1808
- }
1809
- args.push('-a', validateBranchName(name), '-m', message);
1810
- }
1811
- else if (name)
1812
- args.push(validateBranchName(name));
1813
- return {
1814
- content: [
1815
- {
1816
- type: 'text',
1817
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1818
- },
1819
- ],
1820
- };
1821
- },
1822
- },
1823
- // git_remote tool
1824
- {
1825
- name: 'git_remote',
1826
- description: 'Manage set of tracked repositories (list, add, remove, update remotes)',
1827
- inputSchema: {
1828
- type: 'object',
1829
- properties: {
1830
- path: {
1831
- type: 'string',
1832
- description: 'Path to the git repository',
1833
- },
1834
- action: {
1835
- type: 'string',
1836
- enum: ['list', 'add', 'remove', 'rename', 'set-url'],
1837
- description: 'Remote action to perform',
1838
- },
1839
- name: {
1840
- type: 'string',
1841
- description: 'Name of the remote',
1842
- },
1843
- url: {
1844
- type: 'string',
1845
- description: 'URL of the remote repository',
1846
- },
1847
- },
1848
- },
1849
- handler: async (params) => {
1850
- const { path, action, name, url } = params;
1851
- const validatedPath = validatePath(path);
1852
- const args = ['remote'];
1853
- if (action === 'list' || !action)
1854
- args.push('-v');
1855
- else if (action === 'add' && name && url)
1856
- args.push('add', validateRemoteName(name), validateUrl(url));
1857
- else if (action === 'remove' && name)
1858
- args.push('remove', validateRemoteName(name));
1859
- else if (action === 'set-url' && name && url)
1860
- args.push('set-url', validateRemoteName(name), validateUrl(url));
1861
- return {
1862
- content: [
1863
- {
1864
- type: 'text',
1865
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1866
- },
1867
- ],
1868
- };
1869
- },
1870
- },
1871
- // git_fetch tool
1872
- {
1873
- name: 'git_fetch',
1874
- description: 'Fetch branches and tags from one or more remote repositories',
1875
- inputSchema: {
1876
- type: 'object',
1877
- properties: {
1878
- path: {
1879
- type: 'string',
1880
- description: 'Path to the git repository',
1881
- },
1882
- remote: {
1883
- type: 'string',
1884
- description: 'Name of the remote to fetch from',
1885
- },
1886
- all: {
1887
- type: 'boolean',
1888
- description: 'Fetch all remotes',
1889
- },
1890
- prune: {
1891
- type: 'boolean',
1892
- description: 'Prune remote-tracking branches no longer on remote',
1893
- },
1894
- },
1895
- },
1896
- handler: async (params) => {
1897
- const { path, remote, all, prune } = params;
1898
- const validatedPath = validatePath(path);
1899
- const args = ['fetch'];
1900
- if (all)
1901
- args.push('--all');
1902
- if (prune)
1903
- args.push('--prune');
1904
- if (remote && !all)
1905
- args.push(validateRemoteName(remote));
1906
- return {
1907
- content: [
1908
- {
1909
- type: 'text',
1910
- text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1911
- },
1912
- ],
1913
- };
1914
- },
1915
- },
1916
- // git_show tool - uses bash CLI
1917
- {
1918
- name: 'git_show',
1919
- description: 'Show various types of objects (commits, trees, blobs, tags) with their content and metadata',
1920
- inputSchema: {
1921
- type: 'object',
1922
- properties: {
1923
- revision: {
1924
- type: 'string',
1925
- description: 'The revision to show (commit SHA, branch name, tag, HEAD, or revision:path syntax)',
1926
- },
1927
- path: {
1928
- type: 'string',
1929
- description: 'Optional file path to show at the revision',
1930
- },
1931
- format: {
1932
- type: 'string',
1933
- enum: ['commit', 'raw', 'diff'],
1934
- description: 'Output format: commit (default with diff), raw (file content only), diff (diff only)',
1935
- },
1936
- context_lines: {
1937
- type: 'number',
1938
- description: 'Number of context lines for diff output',
1939
- minimum: 0,
1940
- },
1941
- },
1942
- required: ['revision'],
1943
- },
1944
- handler: async (params) => {
1945
- const { revision, path: filePath, format, context_lines } = params;
1946
- const ctx = globalRepositoryContext;
1947
- // Security validation
1948
- if (/[;|&$`<>]/.test(revision)) {
1949
- return {
1950
- content: [{ type: 'text', text: 'Invalid revision: contains forbidden characters' }],
1951
- isError: true,
1952
- };
1953
- }
1954
- if (filePath && (filePath.includes('..') || filePath.startsWith('/') || /[<>|&;$`]/.test(filePath))) {
1955
- return {
1956
- content: [{ type: 'text', text: 'Invalid path: contains forbidden characters' }],
1957
- isError: true,
1958
- };
1959
- }
1960
- if (context_lines !== undefined && context_lines < 0) {
1961
- return {
1962
- content: [{ type: 'text', text: 'Invalid context_lines: must be at least 0' }],
1963
- isError: true,
1964
- };
1965
- }
1966
- // If repository context is set, use it (for testing with mocks)
1967
- if (ctx) {
1968
- try {
1969
- // Parse revision:path syntax
1970
- let targetRevision = revision;
1971
- let targetPath = filePath;
1972
- if (revision.includes(':') && !filePath) {
1973
- const colonIdx = revision.indexOf(':');
1974
- targetRevision = revision.substring(0, colonIdx);
1975
- targetPath = revision.substring(colonIdx + 1);
1976
- }
1977
- // Resolve revision to SHA
1978
- let commitSha = null;
1979
- // Handle HEAD
1980
- if (targetRevision === 'HEAD' || targetRevision.startsWith('HEAD~') || targetRevision.startsWith('HEAD^')) {
1981
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
1982
- if (headRef) {
1983
- commitSha = await ctx.refStore.getRef(headRef);
1984
- }
1985
- else {
1986
- commitSha = await ctx.refStore.getHead();
1987
- }
1988
- // Handle HEAD~n or HEAD^n (simplified - just get parent for now)
1989
- if (commitSha && (targetRevision.includes('~') || targetRevision.includes('^'))) {
1990
- const commit = await ctx.objectStore.getCommit(commitSha);
1991
- if (commit && commit.parents.length > 0) {
1992
- commitSha = commit.parents[0];
1993
- }
1994
- else {
1995
- return {
1996
- content: [{ type: 'text', text: `fatal: bad revision '${targetRevision}'` }],
1997
- isError: true,
1998
- };
1999
- }
2000
- }
2001
- }
2002
- else if (/^[a-f0-9]{7,40}$/i.test(targetRevision)) {
2003
- // Direct SHA (full or abbreviated)
2004
- if (targetRevision.length === 40) {
2005
- commitSha = targetRevision;
2006
- }
2007
- else {
2008
- // Abbreviated SHA - for mock context, try to match
2009
- commitSha = targetRevision; // Mock will handle this
2010
- }
2011
- }
2012
- else {
2013
- // Try as branch
2014
- commitSha = await ctx.refStore.getRef(`refs/heads/${targetRevision}`);
2015
- // Try as tag
2016
- if (!commitSha) {
2017
- commitSha = await ctx.refStore.getRef(`refs/tags/${targetRevision}`);
2018
- }
2019
- }
2020
- if (!commitSha) {
2021
- return {
2022
- content: [{ type: 'text', text: `fatal: bad revision '${targetRevision}'` }],
2023
- isError: true,
2024
- };
2025
- }
2026
- // If path is specified, show file content
2027
- if (targetPath) {
2028
- const commit = await ctx.objectStore.getCommit(commitSha);
2029
- if (!commit) {
2030
- return {
2031
- content: [{ type: 'text', text: `fatal: not a valid object name ${commitSha}` }],
2032
- isError: true,
2033
- };
2034
- }
2035
- const tree = await ctx.objectStore.getTree(commit.tree);
2036
- if (!tree) {
2037
- return {
2038
- content: [{ type: 'text', text: `fatal: tree not found` }],
2039
- isError: true,
2040
- };
2041
- }
2042
- // Find file in tree (simplified - assumes file is at root level)
2043
- const entry = tree.entries.find(e => e.name === targetPath);
2044
- if (!entry) {
2045
- return {
2046
- content: [{ type: 'text', text: `fatal: path '${targetPath}' does not exist in '${targetRevision}'` }],
2047
- isError: true,
2048
- };
2049
- }
2050
- const blob = await ctx.objectStore.getBlob(entry.sha);
2051
- if (!blob) {
2052
- return {
2053
- content: [{ type: 'text', text: `fatal: blob not found` }],
2054
- isError: true,
2055
- };
2056
- }
2057
- // Check for binary content
2058
- const isBinary = blob.some((b, i) => i < 8000 && b === 0);
2059
- if (isBinary) {
2060
- // Return base64 encoded binary content
2061
- const base64 = btoa(String.fromCharCode(...blob));
2062
- return {
2063
- content: [{ type: 'text', text: `Binary file content (base64):\n${base64}` }],
2064
- isError: false,
2065
- };
2066
- }
2067
- const content = new TextDecoder().decode(blob);
2068
- return {
2069
- content: [{ type: 'text', text: format === 'raw' ? content : content }],
2070
- isError: false,
2071
- };
2072
- }
2073
- // Show commit information
2074
- const commit = await ctx.objectStore.getCommit(commitSha);
2075
- if (!commit) {
2076
- return {
2077
- content: [{ type: 'text', text: `fatal: not a valid object name ${commitSha}` }],
2078
- isError: true,
2079
- };
2080
- }
2081
- const lines = [];
2082
- lines.push(`commit ${commitSha}`);
2083
- if (commit.parents.length > 1) {
2084
- lines.push(`Merge: ${commit.parents.join(' ')}`);
2085
- }
2086
- else if (commit.parents.length === 1) {
2087
- lines.push(`parent ${commit.parents[0]}`);
2088
- }
2089
- lines.push(`Author: ${commit.author.name} <${commit.author.email}>`);
2090
- if (commit.committer && commit.committer.name !== commit.author.name) {
2091
- lines.push(`Committer: ${commit.committer.name} <${commit.committer.email}>`);
2092
- }
2093
- else {
2094
- lines.push(`Committer: ${commit.committer?.name || commit.author.name} <${commit.committer?.email || commit.author.email}>`);
2095
- }
2096
- const authorDate = new Date(commit.author.timestamp * 1000);
2097
- // Include timezone in date output
2098
- const timezone = commit.author.timezone || '+0000';
2099
- lines.push(`Date: ${authorDate.toUTCString()} ${timezone}`);
2100
- if (commit.gpgsig) {
2101
- lines.push('');
2102
- lines.push('gpgsig ' + commit.gpgsig);
2103
- }
2104
- lines.push('');
2105
- const messageLines = commit.message.split('\n');
2106
- for (const line of messageLines) {
2107
- lines.push(` ${line}`);
2108
- }
2109
- // Add diff output (simplified)
2110
- if (format !== 'raw') {
2111
- lines.push('');
2112
- const tree = await ctx.objectStore.getTree(commit.tree);
2113
- if (tree) {
2114
- for (const entry of tree.entries) {
2115
- if (entry.mode !== '040000') { // Skip directories
2116
- lines.push(`diff --git a/${entry.name} b/${entry.name}`);
2117
- lines.push(`index 0000000..${entry.sha.substring(0, 7)}`);
2118
- lines.push(`--- /dev/null`);
2119
- lines.push(`+++ b/${entry.name}`);
2120
- const blob = await ctx.objectStore.getBlob(entry.sha);
2121
- if (blob) {
2122
- const content = new TextDecoder().decode(blob);
2123
- const contentLines = content.split('\n');
2124
- lines.push(`@@ -0,0 +1,${contentLines.length} @@`);
2125
- for (const contentLine of contentLines) {
2126
- lines.push(`+${contentLine}`);
2127
- }
2128
- }
2129
- }
2130
- }
2131
- }
2132
- }
2133
- return {
2134
- content: [{ type: 'text', text: lines.join('\n') }],
2135
- isError: false,
2136
- };
2137
- }
2138
- catch (error) {
2139
- return {
2140
- content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2141
- isError: true,
2142
- };
2143
- }
2144
- }
2145
- // Use bash CLI
2146
- const args = ['show'];
2147
- if (format === 'diff') {
2148
- args.push('--format=');
2149
- }
2150
- if (context_lines !== undefined) {
2151
- args.push(`-U${context_lines}`);
2152
- }
2153
- // Handle revision:path syntax
2154
- if (filePath) {
2155
- args.push(`${revision}:${filePath}`);
2156
- }
2157
- else {
2158
- args.push(revision);
2159
- }
2160
- const result = execGit(args);
2161
- if (result.exitCode !== 0) {
2162
- return {
2163
- content: [{ type: 'text', text: result.stderr || `git show failed with exit code ${result.exitCode}` }],
2164
- isError: true,
2165
- };
2166
- }
2167
- return {
2168
- content: [{ type: 'text', text: result.stdout }],
2169
- isError: false,
2170
- };
2171
- },
2172
- },
2173
- // git_blame tool - uses bash CLI
2174
- {
2175
- name: 'git_blame',
2176
- description: 'Git blame - show what revision and author last modified each line of a file',
2177
- inputSchema: {
2178
- type: 'object',
2179
- properties: {
2180
- path: {
2181
- type: 'string',
2182
- description: 'File path to blame',
2183
- },
2184
- revision: {
2185
- type: 'string',
2186
- description: 'Show blame at specific revision (commit SHA, branch, tag)',
2187
- },
2188
- start_line: {
2189
- type: 'number',
2190
- description: 'Start line number (1-indexed)',
2191
- minimum: 1,
2192
- },
2193
- end_line: {
2194
- type: 'number',
2195
- description: 'End line number (1-indexed, inclusive)',
2196
- minimum: 1,
2197
- },
2198
- show_email: {
2199
- type: 'boolean',
2200
- description: 'Show author email instead of name',
2201
- },
2202
- show_stats: {
2203
- type: 'boolean',
2204
- description: 'Show statistics summary',
2205
- },
2206
- },
2207
- required: ['path'],
2208
- },
2209
- handler: async (params) => {
2210
- const { path: filePath, revision, start_line, end_line, show_email } = params;
2211
- const ctx = globalRepositoryContext;
2212
- // Security validation
2213
- if (filePath.includes('..') || filePath.startsWith('/') || /[<>|&;$`]/.test(filePath)) {
2214
- return {
2215
- content: [{ type: 'text', text: 'Invalid path: contains forbidden characters' }],
2216
- isError: true,
2217
- };
2218
- }
2219
- if (revision && /[;|&$`<>]/.test(revision)) {
2220
- return {
2221
- content: [{ type: 'text', text: 'Invalid revision: contains forbidden characters' }],
2222
- isError: true,
2223
- };
2224
- }
2225
- if (start_line !== undefined && start_line < 1) {
2226
- return {
2227
- content: [{ type: 'text', text: 'Invalid start_line: must be at least 1' }],
2228
- isError: true,
2229
- };
2230
- }
2231
- if (end_line !== undefined && end_line < 1) {
2232
- return {
2233
- content: [{ type: 'text', text: 'Invalid end_line: must be at least 1' }],
2234
- isError: true,
2235
- };
2236
- }
2237
- if (start_line !== undefined && end_line !== undefined && start_line > end_line) {
2238
- return {
2239
- content: [{ type: 'text', text: 'Invalid line range: start_line cannot be greater than end_line' }],
2240
- isError: true,
2241
- };
2242
- }
2243
- // If repository context is set, use it (for testing with mocks)
2244
- if (ctx) {
2245
- try {
2246
- // Resolve revision to SHA
2247
- let commitSha = null;
2248
- if (revision) {
2249
- if (revision === 'HEAD' || revision.startsWith('HEAD~') || revision.startsWith('HEAD^')) {
2250
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
2251
- if (headRef) {
2252
- commitSha = await ctx.refStore.getRef(headRef);
2253
- }
2254
- else {
2255
- commitSha = await ctx.refStore.getHead();
2256
- }
2257
- }
2258
- else if (/^[a-f0-9]{7,40}$/i.test(revision)) {
2259
- commitSha = revision.length === 40 ? revision : revision;
2260
- }
2261
- else {
2262
- commitSha = await ctx.refStore.getRef(`refs/heads/${revision}`);
2263
- if (!commitSha) {
2264
- commitSha = await ctx.refStore.getRef(`refs/tags/${revision}`);
2265
- }
2266
- }
2267
- }
2268
- else {
2269
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
2270
- if (headRef) {
2271
- commitSha = await ctx.refStore.getRef(headRef);
2272
- }
2273
- else {
2274
- commitSha = await ctx.refStore.getHead();
2275
- }
2276
- }
2277
- if (!commitSha) {
2278
- return {
2279
- content: [{ type: 'text', text: `fatal: bad revision '${revision || 'HEAD'}'` }],
2280
- isError: true,
2281
- };
2282
- }
2283
- // Get commit and find file
2284
- const commit = await ctx.objectStore.getCommit(commitSha);
2285
- if (!commit) {
2286
- return {
2287
- content: [{ type: 'text', text: `fatal: not a valid object name ${commitSha}` }],
2288
- isError: true,
2289
- };
2290
- }
2291
- const tree = await ctx.objectStore.getTree(commit.tree);
2292
- if (!tree) {
2293
- return {
2294
- content: [{ type: 'text', text: 'fatal: tree not found' }],
2295
- isError: true,
2296
- };
2297
- }
2298
- // Find file in tree (handles nested paths)
2299
- // First, try finding the exact path as a flat entry (for mocks with flat structure)
2300
- let blobSha = null;
2301
- const flatEntry = tree.entries.find(e => e.name === filePath);
2302
- if (flatEntry && flatEntry.mode !== '040000') {
2303
- blobSha = flatEntry.sha;
2304
- }
2305
- // If not found as flat, try navigating nested structure
2306
- if (!blobSha) {
2307
- const pathParts = filePath.split('/');
2308
- let currentTree = tree;
2309
- for (let i = 0; i < pathParts.length; i++) {
2310
- const part = pathParts[i];
2311
- const entry = currentTree.entries.find(e => e.name === part);
2312
- if (!entry) {
2313
- return {
2314
- content: [{ type: 'text', text: `fatal: no such path '${filePath}' in HEAD` }],
2315
- isError: true,
2316
- };
2317
- }
2318
- if (i === pathParts.length - 1) {
2319
- // Last part - should be a file
2320
- if (entry.mode === '040000') {
2321
- return {
2322
- content: [{ type: 'text', text: `fatal: '${filePath}' is a directory` }],
2323
- isError: true,
2324
- };
2325
- }
2326
- blobSha = entry.sha;
2327
- }
2328
- else {
2329
- // Intermediate part - should be a directory
2330
- if (entry.mode !== '040000') {
2331
- return {
2332
- content: [{ type: 'text', text: `fatal: '${pathParts.slice(0, i + 1).join('/')}' is not a directory` }],
2333
- isError: true,
2334
- };
2335
- }
2336
- const nextTree = await ctx.objectStore.getTree(entry.sha);
2337
- if (!nextTree) {
2338
- return {
2339
- content: [{ type: 'text', text: 'fatal: tree not found' }],
2340
- isError: true,
2341
- };
2342
- }
2343
- currentTree = nextTree;
2344
- }
2345
- }
2346
- }
2347
- if (!blobSha) {
2348
- return {
2349
- content: [{ type: 'text', text: `fatal: no such path '${filePath}' in HEAD` }],
2350
- isError: true,
2351
- };
2352
- }
2353
- const blob = await ctx.objectStore.getBlob(blobSha);
2354
- if (!blob) {
2355
- return {
2356
- content: [{ type: 'text', text: 'fatal: blob not found' }],
2357
- isError: true,
2358
- };
2359
- }
2360
- // Check for binary content (null bytes or binary file signatures)
2361
- const hasNullBytes = blob.some((b, i) => i < 8000 && b === 0);
2362
- // Check for common binary file signatures
2363
- const isPNG = blob[0] === 0x89 && blob[1] === 0x50 && blob[2] === 0x4e && blob[3] === 0x47;
2364
- const isJPEG = blob[0] === 0xff && blob[1] === 0xd8 && blob[2] === 0xff;
2365
- const isGIF = blob[0] === 0x47 && blob[1] === 0x49 && blob[2] === 0x46;
2366
- const isPDF = blob[0] === 0x25 && blob[1] === 0x50 && blob[2] === 0x44 && blob[3] === 0x46;
2367
- const isBinary = hasNullBytes || isPNG || isJPEG || isGIF || isPDF;
2368
- if (isBinary) {
2369
- return {
2370
- content: [{ type: 'text', text: 'fatal: binary file cannot be blamed' }],
2371
- isError: true,
2372
- };
2373
- }
2374
- const content = new TextDecoder().decode(blob);
2375
- const lines = content.split('\n');
2376
- if (lines.length > 0 && lines[lines.length - 1] === '') {
2377
- lines.pop();
2378
- }
2379
- // Apply line range filter
2380
- let startIdx = 0;
2381
- let endIdx = lines.length;
2382
- if (start_line !== undefined) {
2383
- startIdx = start_line - 1;
2384
- }
2385
- if (end_line !== undefined) {
2386
- endIdx = Math.min(end_line, lines.length);
2387
- }
2388
- const filteredLines = lines.slice(startIdx, endIdx);
2389
- // Format blame output
2390
- const date = new Date(commit.author.timestamp * 1000);
2391
- const dateStr = date.toISOString().substring(0, 10);
2392
- const authorName = commit.author.name.padEnd(15).substring(0, 15);
2393
- const shortSha = commitSha.substring(0, 8);
2394
- const outputLines = filteredLines.map((line, idx) => {
2395
- const lineNum = startIdx + idx + 1;
2396
- if (show_email) {
2397
- return `${shortSha} (${commit.author.email.padEnd(20).substring(0, 20)} ${dateStr} ${lineNum.toString().padStart(4)}) ${line}`;
2398
- }
2399
- return `${shortSha} (${authorName} ${dateStr} ${lineNum.toString().padStart(4)}) ${line}`;
2400
- });
2401
- return {
2402
- content: [{ type: 'text', text: outputLines.join('\n') }],
2403
- isError: false,
2404
- };
2405
- }
2406
- catch (error) {
2407
- return {
2408
- content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2409
- isError: true,
2410
- };
2411
- }
2412
- }
2413
- // Use bash CLI
2414
- const args = ['blame'];
2415
- if (show_email) {
2416
- args.push('-e');
2417
- }
2418
- if (start_line !== undefined || end_line !== undefined) {
2419
- const start = start_line || 1;
2420
- const end = end_line || '';
2421
- args.push(`-L${start},${end}`);
2422
- }
2423
- if (revision) {
2424
- args.push(revision);
2425
- }
2426
- args.push('--', filePath);
2427
- const result = execGit(args);
2428
- if (result.exitCode !== 0) {
2429
- return {
2430
- content: [{ type: 'text', text: result.stderr || `git blame failed with exit code ${result.exitCode}` }],
2431
- isError: true,
2432
- };
2433
- }
2434
- return {
2435
- content: [{ type: 'text', text: result.stdout }],
2436
- isError: false,
2437
- };
2438
- },
2439
- },
2440
- // git_ls_tree tool - uses bash CLI
2441
- {
2442
- name: 'git_ls_tree',
2443
- description: 'List the contents of a tree object, showing file names, modes, types, and SHA hashes',
2444
- inputSchema: {
2445
- type: 'object',
2446
- properties: {
2447
- tree_ish: {
2448
- type: 'string',
2449
- description: 'Tree-ish to list (commit SHA, branch, tag, tree SHA)',
2450
- },
2451
- path: {
2452
- type: 'string',
2453
- description: 'Optional path filter within the tree',
2454
- },
2455
- recursive: {
2456
- type: 'boolean',
2457
- description: 'Recurse into subdirectories',
2458
- },
2459
- show_trees: {
2460
- type: 'boolean',
2461
- description: 'Show only tree entries (directories), like -d flag',
2462
- },
2463
- show_size: {
2464
- type: 'boolean',
2465
- description: 'Show object size for blob entries',
2466
- },
2467
- name_only: {
2468
- type: 'boolean',
2469
- description: 'Show only file names without mode, type, or SHA',
2470
- },
2471
- },
2472
- required: ['tree_ish'],
2473
- },
2474
- handler: async (params) => {
2475
- const { tree_ish, path: filterPath, recursive, show_trees, show_size, name_only } = params;
2476
- const ctx = globalRepositoryContext;
2477
- // Security validation
2478
- if (/[;|&$`<>]/.test(tree_ish)) {
2479
- return {
2480
- content: [{ type: 'text', text: 'Invalid tree_ish: contains forbidden characters' }],
2481
- isError: true,
2482
- };
2483
- }
2484
- if (filterPath && (filterPath.includes('..') || /[<>|&;$`]/.test(filterPath))) {
2485
- return {
2486
- content: [{ type: 'text', text: 'Invalid path: contains forbidden characters' }],
2487
- isError: true,
2488
- };
2489
- }
2490
- // If repository context is set, use it (for testing with mocks)
2491
- if (ctx) {
2492
- try {
2493
- // Resolve tree_ish to tree SHA
2494
- let treeSha = null;
2495
- // Try direct tree SHA first
2496
- if (/^[a-f0-9]{40}$/i.test(tree_ish)) {
2497
- const obj = await ctx.objectStore.getObject(tree_ish);
2498
- if (obj?.type === 'tree') {
2499
- treeSha = tree_ish;
2500
- }
2501
- else if (obj?.type === 'commit') {
2502
- const commit = await ctx.objectStore.getCommit(tree_ish);
2503
- treeSha = commit?.tree || null;
2504
- }
2505
- }
2506
- // Try as HEAD or branch/tag reference
2507
- if (!treeSha) {
2508
- let commitSha = null;
2509
- if (tree_ish === 'HEAD') {
2510
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
2511
- if (headRef) {
2512
- commitSha = await ctx.refStore.getRef(headRef);
2513
- }
2514
- else {
2515
- commitSha = await ctx.refStore.getHead();
2516
- }
2517
- }
2518
- else {
2519
- commitSha = await ctx.refStore.getRef(`refs/heads/${tree_ish}`);
2520
- if (!commitSha) {
2521
- commitSha = await ctx.refStore.getRef(`refs/tags/${tree_ish}`);
2522
- }
2523
- }
2524
- if (commitSha) {
2525
- const commit = await ctx.objectStore.getCommit(commitSha);
2526
- treeSha = commit?.tree || null;
2527
- }
2528
- }
2529
- if (!treeSha) {
2530
- return {
2531
- content: [{ type: 'text', text: `fatal: not a valid object name '${tree_ish}'` }],
2532
- isError: true,
2533
- };
2534
- }
2535
- // Navigate to path if specified
2536
- if (filterPath) {
2537
- const pathParts = filterPath.replace(/\/$/, '').split('/');
2538
- let currentTreeSha = treeSha;
2539
- for (const part of pathParts) {
2540
- const tree = await ctx.objectStore.getTree(currentTreeSha);
2541
- if (!tree) {
2542
- return {
2543
- content: [{ type: 'text', text: `fatal: path '${filterPath}' does not exist` }],
2544
- isError: true,
2545
- };
2546
- }
2547
- const entry = tree.entries.find(e => e.name === part);
2548
- if (!entry) {
2549
- return {
2550
- content: [{ type: 'text', text: `fatal: path '${filterPath}' does not exist` }],
2551
- isError: true,
2552
- };
2553
- }
2554
- if (entry.mode === '040000') {
2555
- currentTreeSha = entry.sha;
2556
- }
2557
- else {
2558
- // It's a file - show just this entry
2559
- let output = '';
2560
- if (name_only) {
2561
- output = entry.name;
2562
- }
2563
- else {
2564
- const typeStr = entry.mode === '040000' ? 'tree' :
2565
- entry.mode === '160000' ? 'commit' : 'blob';
2566
- output = `${entry.mode} ${typeStr} ${entry.sha}\t${entry.name}`;
2567
- }
2568
- return { content: [{ type: 'text', text: output }], isError: false };
2569
- }
2570
- }
2571
- treeSha = currentTreeSha;
2572
- }
2573
- // List tree contents
2574
- const entries = [];
2575
- async function listTree(sha, prefix) {
2576
- const tree = await ctx.objectStore.getTree(sha);
2577
- if (!tree)
2578
- return;
2579
- for (const entry of tree.entries) {
2580
- const fullPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2581
- const typeStr = entry.mode === '040000' ? 'tree' :
2582
- entry.mode === '160000' ? 'commit' : 'blob';
2583
- if (show_trees) {
2584
- // Only show tree entries
2585
- if (typeStr === 'tree') {
2586
- entries.push({ mode: entry.mode, type: typeStr, sha: entry.sha, name: entry.name, path: fullPath });
2587
- if (recursive) {
2588
- await listTree(entry.sha, fullPath);
2589
- }
2590
- }
2591
- }
2592
- else {
2593
- if (typeStr === 'tree') {
2594
- if (recursive) {
2595
- await listTree(entry.sha, fullPath);
2596
- }
2597
- else {
2598
- entries.push({ mode: entry.mode, type: typeStr, sha: entry.sha, name: entry.name, path: fullPath });
2599
- }
2600
- }
2601
- else {
2602
- let size;
2603
- if (show_size && typeStr === 'blob') {
2604
- const blob = await ctx.objectStore.getBlob(entry.sha);
2605
- size = blob?.length;
2606
- }
2607
- entries.push({ mode: entry.mode, type: typeStr, sha: entry.sha, name: entry.name, path: fullPath, size });
2608
- }
2609
- }
2610
- }
2611
- }
2612
- await listTree(treeSha, '');
2613
- // Format output
2614
- const outputLines = entries.map(e => {
2615
- if (name_only) {
2616
- return e.path;
2617
- }
2618
- if (show_size) {
2619
- const sizeStr = e.type === 'tree' ? '-' : (e.size?.toString() || '0');
2620
- return `${e.mode} ${e.type} ${e.sha} ${sizeStr.padStart(7)}\t${e.path}`;
2621
- }
2622
- return `${e.mode} ${e.type} ${e.sha}\t${e.path}`;
2623
- });
2624
- return {
2625
- content: [{ type: 'text', text: outputLines.join('\n') }],
2626
- isError: false,
2627
- };
2628
- }
2629
- catch (error) {
2630
- return {
2631
- content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2632
- isError: true,
2633
- };
2634
- }
2635
- }
2636
- // Use bash CLI
2637
- const args = ['ls-tree'];
2638
- if (recursive) {
2639
- args.push('-r');
2640
- }
2641
- if (show_trees) {
2642
- args.push('-d');
2643
- }
2644
- if (show_size) {
2645
- args.push('-l');
2646
- }
2647
- if (name_only) {
2648
- args.push('--name-only');
2649
- }
2650
- args.push(tree_ish);
2651
- if (filterPath) {
2652
- args.push('--', filterPath);
2653
- }
2654
- const result = execGit(args);
2655
- if (result.exitCode !== 0) {
2656
- return {
2657
- content: [{ type: 'text', text: result.stderr || `git ls-tree failed with exit code ${result.exitCode}` }],
2658
- isError: true,
2659
- };
2660
- }
2661
- return {
2662
- content: [{ type: 'text', text: result.stdout }],
2663
- isError: false,
2664
- };
2665
- },
2666
- },
2667
- // git_cat_file tool - uses bash CLI
2668
- {
2669
- name: 'git_cat_file',
2670
- description: 'Show content or type/size information for repository objects',
2671
- inputSchema: {
2672
- type: 'object',
2673
- properties: {
2674
- object: {
2675
- type: 'string',
2676
- description: 'Object SHA or reference to inspect',
2677
- },
2678
- type: {
2679
- type: 'string',
2680
- enum: ['blob', 'tree', 'commit', 'tag', 'auto'],
2681
- description: 'Expected object type (auto to detect)',
2682
- },
2683
- pretty_print: {
2684
- type: 'boolean',
2685
- description: 'Pretty-print the object content',
2686
- },
2687
- show_size: {
2688
- type: 'boolean',
2689
- description: 'Show only the object size',
2690
- },
2691
- show_type: {
2692
- type: 'boolean',
2693
- description: 'Show only the object type',
2694
- },
2695
- },
2696
- required: ['object'],
2697
- },
2698
- handler: async (params) => {
2699
- const { object: objectRef, type: expectedType, pretty_print, show_size, show_type } = params;
2700
- const ctx = globalRepositoryContext;
2701
- // Security validation
2702
- if (/[;|&$`<>]/.test(objectRef)) {
2703
- return {
2704
- content: [{ type: 'text', text: 'Invalid object: contains forbidden characters' }],
2705
- isError: true,
2706
- };
2707
- }
2708
- // If repository context is set, use it (for testing with mocks)
2709
- if (ctx) {
2710
- try {
2711
- // Resolve object reference to SHA
2712
- let objectSha = null;
2713
- if (objectRef === 'HEAD') {
2714
- const headRef = await ctx.refStore.getSymbolicRef('HEAD');
2715
- if (headRef) {
2716
- objectSha = await ctx.refStore.getRef(headRef);
2717
- }
2718
- else {
2719
- objectSha = await ctx.refStore.getHead();
2720
- }
2721
- }
2722
- else {
2723
- // First try direct object lookup (for testing with mock SHAs)
2724
- if (await ctx.objectStore.hasObject(objectRef)) {
2725
- objectSha = objectRef;
2726
- }
2727
- else if (/^[a-f0-9]{7,40}$/i.test(objectRef)) {
2728
- // Try abbreviated SHA - for mock, check if it starts with the ref
2729
- if (objectRef.length < 40) {
2730
- // Search for matching object in mock (simplified)
2731
- const hasObj = await ctx.objectStore.hasObject(objectRef + 'blob');
2732
- if (hasObj) {
2733
- objectSha = objectRef + 'blob';
2734
- }
2735
- else {
2736
- objectSha = objectRef;
2737
- }
2738
- }
2739
- else {
2740
- objectSha = objectRef;
2741
- }
2742
- }
2743
- else {
2744
- // Try as branch/tag
2745
- objectSha = await ctx.refStore.getRef(`refs/heads/${objectRef}`);
2746
- if (!objectSha) {
2747
- objectSha = await ctx.refStore.getRef(`refs/tags/${objectRef}`);
2748
- }
2749
- }
2750
- }
2751
- if (!objectSha) {
2752
- return {
2753
- content: [{ type: 'text', text: `fatal: Not a valid object name ${objectRef}` }],
2754
- isError: true,
2755
- };
2756
- }
2757
- const obj = await ctx.objectStore.getObject(objectSha);
2758
- if (!obj) {
2759
- return {
2760
- content: [{ type: 'text', text: `fatal: Not a valid object name ${objectRef}` }],
2761
- isError: true,
2762
- };
2763
- }
2764
- // Check type mismatch
2765
- if (expectedType && expectedType !== 'auto' && obj.type !== expectedType) {
2766
- return {
2767
- content: [{ type: 'text', text: `fatal: object type mismatch: expected ${expectedType}, got ${obj.type}` }],
2768
- isError: true,
2769
- };
2770
- }
2771
- // Show type only
2772
- if (show_type) {
2773
- return {
2774
- content: [{ type: 'text', text: obj.type }],
2775
- isError: false,
2776
- };
2777
- }
2778
- // Show size only
2779
- if (show_size) {
2780
- return {
2781
- content: [{ type: 'text', text: obj.data.length.toString() }],
2782
- isError: false,
2783
- };
2784
- }
2785
- // Show content based on type
2786
- if (obj.type === 'blob') {
2787
- const content = new TextDecoder().decode(obj.data);
2788
- return {
2789
- content: [{ type: 'text', text: content }],
2790
- isError: false,
2791
- };
2792
- }
2793
- if (obj.type === 'tree') {
2794
- const tree = await ctx.objectStore.getTree(objectSha);
2795
- if (!tree) {
2796
- return {
2797
- content: [{ type: 'text', text: 'fatal: unable to read tree' }],
2798
- isError: true,
2799
- };
2800
- }
2801
- const lines = tree.entries.map(e => {
2802
- const typeStr = e.mode === '040000' ? 'tree' :
2803
- e.mode === '160000' ? 'commit' : 'blob';
2804
- return `${e.mode} ${typeStr} ${e.sha}\t${e.name}`;
2805
- });
2806
- return {
2807
- content: [{ type: 'text', text: lines.join('\n') }],
2808
- isError: false,
2809
- };
2810
- }
2811
- if (obj.type === 'commit') {
2812
- const commit = await ctx.objectStore.getCommit(objectSha);
2813
- if (!commit) {
2814
- return {
2815
- content: [{ type: 'text', text: 'fatal: unable to read commit' }],
2816
- isError: true,
2817
- };
2818
- }
2819
- const lines = [];
2820
- lines.push(`tree ${commit.tree}`);
2821
- for (const parent of commit.parents) {
2822
- lines.push(`parent ${parent}`);
2823
- }
2824
- lines.push(`author ${commit.author.name} <${commit.author.email}> ${commit.author.timestamp} ${commit.author.timezone}`);
2825
- lines.push(`committer ${commit.committer?.name || commit.author.name} <${commit.committer?.email || commit.author.email}> ${commit.committer?.timestamp || commit.author.timestamp} ${commit.committer?.timezone || commit.author.timezone}`);
2826
- if (commit.gpgsig) {
2827
- lines.push(`gpgsig ${commit.gpgsig}`);
2828
- }
2829
- lines.push('');
2830
- lines.push(commit.message);
2831
- return {
2832
- content: [{ type: 'text', text: lines.join('\n') }],
2833
- isError: false,
2834
- };
2835
- }
2836
- // Default - show raw data
2837
- return {
2838
- content: [{ type: 'text', text: new TextDecoder().decode(obj.data) }],
2839
- isError: false,
2840
- };
2841
- }
2842
- catch (error) {
2843
- return {
2844
- content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2845
- isError: true,
2846
- };
2847
- }
2848
- }
2849
- // Use bash CLI
2850
- const args = ['cat-file'];
2851
- if (show_type) {
2852
- args.push('-t');
2853
- }
2854
- else if (show_size) {
2855
- args.push('-s');
2856
- }
2857
- else if (pretty_print) {
2858
- args.push('-p');
2859
- }
2860
- else if (expectedType && expectedType !== 'auto') {
2861
- args.push(expectedType);
2862
- }
2863
- else {
2864
- args.push('-p');
2865
- }
2866
- args.push(objectRef);
2867
- const result = execGit(args);
2868
- if (result.exitCode !== 0) {
2869
- return {
2870
- content: [{ type: 'text', text: result.stderr || `git cat-file failed with exit code ${result.exitCode}` }],
2871
- isError: true,
2872
- };
2873
- }
2874
- return {
2875
- content: [{ type: 'text', text: result.stdout }],
2876
- isError: false,
2877
- };
2878
- },
2879
- },
2880
- ];
2881
- // Register all git tools in the registry on module load
2882
- gitTools.forEach((tool) => {
2883
- toolRegistry.set(tool.name, tool);
2884
- });
2885
- /**
2886
- * Register a new tool in the registry.
2887
- *
2888
- * @description
2889
- * Adds a custom tool to the global tool registry. The tool must have a valid
2890
- * handler function and a unique name. Once registered, the tool can be invoked
2891
- * using {@link invokeTool}.
2892
- *
2893
- * Note: Built-in git tools are automatically registered on module load.
2894
- *
2895
- * @param tool - The tool definition to register
2896
- * @returns void
2897
- * @throws {Error} If tool handler is missing or not a function
2898
- * @throws {Error} If a tool with the same name already exists
2899
- *
2900
- * @example
2901
- * import { registerTool, invokeTool } from './tools'
2902
- *
2903
- * // Register a custom tool
2904
- * registerTool({
2905
- * name: 'custom_operation',
2906
- * description: 'Performs a custom operation',
2907
- * inputSchema: {
2908
- * type: 'object',
2909
- * properties: {
2910
- * value: { type: 'string', description: 'Input value' }
2911
- * },
2912
- * required: ['value']
2913
- * },
2914
- * handler: async (params) => {
2915
- * const { value } = params as { value: string }
2916
- * return {
2917
- * content: [{ type: 'text', text: `Processed: ${value}` }]
2918
- * }
2919
- * }
2920
- * })
2921
- *
2922
- * // Now invoke the registered tool
2923
- * const result = await invokeTool('custom_operation', { value: 'test' })
2924
- */
2925
- export function registerTool(tool) {
2926
- if (!tool.handler || typeof tool.handler !== 'function') {
2927
- throw new Error(`Tool '${tool.name}' must have a handler function`);
2928
- }
2929
- if (toolRegistry.has(tool.name)) {
2930
- throw new Error(`Tool with name '${tool.name}' already exists (duplicate)`);
2931
- }
2932
- toolRegistry.set(tool.name, tool);
2933
- }
2934
- /**
2935
- * Validate input parameters against a tool's schema.
2936
- *
2937
- * @description
2938
- * Performs comprehensive validation of tool parameters against the tool's
2939
- * JSON Schema definition. Checks for required parameters, type correctness,
2940
- * enum values, numeric constraints, string patterns, and array item types.
2941
- *
2942
- * This function is called automatically by {@link invokeTool} before
2943
- * executing a tool handler, but can also be used independently for
2944
- * pre-validation.
2945
- *
2946
- * @param tool - The tool whose schema to validate against
2947
- * @param params - The parameters to validate
2948
- * @returns Validation result object with valid flag and array of error messages
2949
- *
2950
- * @example
2951
- * import { validateToolInput, getTool } from './tools'
2952
- *
2953
- * const tool = getTool('git_commit')
2954
- * if (tool) {
2955
- * const validation = validateToolInput(tool, { path: '/repo' })
2956
- * if (!validation.valid) {
2957
- * console.error('Validation errors:', validation.errors)
2958
- * // Output: ['Missing required parameter: message']
2959
- * }
2960
- * }
2961
- *
2962
- * @example
2963
- * // Type validation example
2964
- * const result = validateToolInput(tool, { maxCount: 'not-a-number' })
2965
- * // result.errors: ["Parameter 'maxCount' has invalid type: expected number, got string"]
2966
- */
2967
- export function validateToolInput(tool, params) {
2968
- const errors = [];
2969
- const schema = tool.inputSchema;
2970
- // Check required parameters
2971
- if (schema.required) {
2972
- for (const requiredParam of schema.required) {
2973
- if (!(requiredParam in params) || params[requiredParam] === undefined) {
2974
- errors.push(`Missing required parameter: ${requiredParam}`);
2975
- }
2976
- }
2977
- }
2978
- // Check parameter types
2979
- if (schema.properties) {
2980
- for (const [key, value] of Object.entries(params)) {
2981
- const propSchema = schema.properties[key];
2982
- if (!propSchema) {
2983
- // Unknown parameter - could be an error or we could ignore it
2984
- continue;
2985
- }
2986
- // Type validation
2987
- const valueType = Array.isArray(value) ? 'array' : typeof value;
2988
- if (propSchema.type && valueType !== propSchema.type) {
2989
- errors.push(`Parameter '${key}' has invalid type: expected ${propSchema.type}, got ${valueType}`);
2990
- }
2991
- // Enum validation
2992
- if (propSchema.enum && !propSchema.enum.includes(value)) {
2993
- errors.push(`Parameter '${key}' must be one of: ${propSchema.enum.join(', ')}`);
2994
- }
2995
- // Number constraints
2996
- if (propSchema.type === 'number' && typeof value === 'number') {
2997
- if (propSchema.minimum !== undefined && value < propSchema.minimum) {
2998
- errors.push(`Parameter '${key}' must be at least ${propSchema.minimum}`);
2999
- }
3000
- if (propSchema.maximum !== undefined && value > propSchema.maximum) {
3001
- errors.push(`Parameter '${key}' must be at most ${propSchema.maximum}`);
3002
- }
3003
- }
3004
- // String pattern validation
3005
- if (propSchema.type === 'string' && typeof value === 'string' && propSchema.pattern) {
3006
- const regex = new RegExp(propSchema.pattern);
3007
- if (!regex.test(value)) {
3008
- errors.push(`Parameter '${key}' does not match required pattern: ${propSchema.pattern}`);
3009
- }
3010
- }
3011
- // Array item type validation
3012
- if (propSchema.type === 'array' && Array.isArray(value) && propSchema.items) {
3013
- const itemType = propSchema.items.type;
3014
- for (let i = 0; i < value.length; i++) {
3015
- const itemValueType = typeof value[i];
3016
- if (itemType && itemValueType !== itemType) {
3017
- errors.push(`Array item at index ${i} in '${key}' has invalid type: expected ${itemType}, got ${itemValueType}`);
3018
- }
3019
- }
3020
- }
3021
- }
3022
- }
3023
- return {
3024
- valid: errors.length === 0,
3025
- errors,
3026
- };
3027
- }
3028
- /**
3029
- * Invoke a tool by name with the given parameters.
3030
- *
3031
- * @description
3032
- * Looks up a tool by name in the registry, validates the provided parameters
3033
- * against the tool's schema, and executes the tool's handler. Validation
3034
- * errors and execution errors are returned as MCPToolResult with isError=true
3035
- * rather than throwing exceptions.
3036
- *
3037
- * This is the primary function for executing MCP tools. Ensure the repository
3038
- * context is set via {@link setRepositoryContext} before invoking git tools.
3039
- *
3040
- * @param toolName - Name of the tool to invoke (e.g., 'git_status')
3041
- * @param params - Parameters to pass to the tool handler
3042
- * @returns Promise resolving to the tool result
3043
- * @throws {Error} If the tool is not found in the registry
3044
- *
3045
- * @example
3046
- * import { invokeTool, setRepositoryContext } from './tools'
3047
- *
3048
- * // Set up repository context first
3049
- * setRepositoryContext(myRepoContext)
3050
- *
3051
- * // Invoke git_status tool
3052
- * const status = await invokeTool('git_status', { short: true })
3053
- * if (!status.isError) {
3054
- * console.log(status.content[0].text)
3055
- * }
3056
- *
3057
- * @example
3058
- * // Invoke git_log with parameters
3059
- * const log = await invokeTool('git_log', {
3060
- * maxCount: 10,
3061
- * oneline: true,
3062
- * ref: 'main'
3063
- * })
3064
- *
3065
- * @example
3066
- * // Handle validation errors
3067
- * const result = await invokeTool('git_commit', {})
3068
- * if (result.isError) {
3069
- * // result.content[0].text contains validation error message
3070
- * console.error('Error:', result.content[0].text)
3071
- * }
3072
- */
3073
- export async function invokeTool(toolName, params) {
3074
- const tool = toolRegistry.get(toolName);
3075
- if (!tool) {
3076
- throw new Error(`Tool '${toolName}' not found (does not exist)`);
3077
- }
3078
- // Validate parameters before invoking
3079
- const validation = validateToolInput(tool, params);
3080
- if (!validation.valid) {
3081
- return {
3082
- content: [
3083
- {
3084
- type: 'text',
3085
- text: `Validation error: ${validation.errors.join('; ')}`,
3086
- },
3087
- ],
3088
- isError: true,
3089
- };
3090
- }
3091
- // Invoke the handler with error handling
3092
- try {
3093
- return await tool.handler(params);
3094
- }
3095
- catch (error) {
3096
- return {
3097
- content: [
3098
- {
3099
- type: 'text',
3100
- text: error instanceof Error ? error.message : String(error),
3101
- },
3102
- ],
3103
- isError: true,
3104
- };
3105
- }
3106
- }
3107
- /**
3108
- * Get a list of all registered tools.
3109
- *
3110
- * @description
3111
- * Returns an array of all tools in the registry with their names, descriptions,
3112
- * and input schemas. Handler functions are omitted for security and serialization.
3113
- * This is useful for discovery and documentation purposes.
3114
- *
3115
- * @returns Array of tool definitions without handler functions
3116
- *
3117
- * @example
3118
- * import { listTools } from './tools'
3119
- *
3120
- * const tools = listTools()
3121
- * console.log(`Available tools: ${tools.length}`)
3122
- *
3123
- * for (const tool of tools) {
3124
- * console.log(`- ${tool.name}: ${tool.description}`)
3125
- * console.log(` Required params: ${tool.inputSchema.required?.join(', ') || 'none'}`)
3126
- * }
3127
- */
3128
- export function listTools() {
3129
- const tools = [];
3130
- for (const tool of toolRegistry.values()) {
3131
- // Return tool without handler
3132
- tools.push({
3133
- name: tool.name,
3134
- description: tool.description,
3135
- inputSchema: tool.inputSchema,
3136
- });
3137
- }
3138
- return tools;
3139
- }
3140
- /**
3141
- * Get a tool by name.
3142
- *
3143
- * @description
3144
- * Retrieves a tool definition from the registry by its name. Returns the
3145
- * complete tool object including the handler function. Returns undefined
3146
- * if no tool with the given name exists.
3147
- *
3148
- * @param name - Name of the tool to retrieve (e.g., 'git_status')
3149
- * @returns The complete tool definition if found, undefined otherwise
3150
- *
3151
- * @example
3152
- * import { getTool } from './tools'
3153
- *
3154
- * const statusTool = getTool('git_status')
3155
- * if (statusTool) {
3156
- * console.log(`Description: ${statusTool.description}`)
3157
- * console.log(`Parameters:`, Object.keys(statusTool.inputSchema.properties || {}))
3158
- * }
3159
- *
3160
- * @example
3161
- * // Check if a tool exists before using it
3162
- * const tool = getTool('my_custom_tool')
3163
- * if (!tool) {
3164
- * console.error('Tool not found')
3165
- * }
3166
- */
3167
- export function getTool(name) {
3168
- return toolRegistry.get(name);
3169
- }
3170
- //# sourceMappingURL=tools.js.map