@opencodehub/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +62 -0
  2. package/dist/analysis-bridge.d.ts +23 -0
  3. package/dist/analysis-bridge.d.ts.map +1 -0
  4. package/dist/analysis-bridge.js +83 -0
  5. package/dist/analysis-bridge.js.map +1 -0
  6. package/dist/connection-pool.d.ts +76 -0
  7. package/dist/connection-pool.d.ts.map +1 -0
  8. package/dist/connection-pool.js +179 -0
  9. package/dist/connection-pool.js.map +1 -0
  10. package/dist/error-envelope.d.ts +97 -0
  11. package/dist/error-envelope.d.ts.map +1 -0
  12. package/dist/error-envelope.js +75 -0
  13. package/dist/error-envelope.js.map +1 -0
  14. package/dist/group-resolver.d.ts +29 -0
  15. package/dist/group-resolver.d.ts.map +1 -0
  16. package/dist/group-resolver.js +100 -0
  17. package/dist/group-resolver.js.map +1 -0
  18. package/dist/index.d.ts +43 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +54 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/next-step-hints.d.ts +24 -0
  23. package/dist/next-step-hints.d.ts.map +1 -0
  24. package/dist/next-step-hints.js +41 -0
  25. package/dist/next-step-hints.js.map +1 -0
  26. package/dist/repo-resolver.d.ts +88 -0
  27. package/dist/repo-resolver.d.ts.map +1 -0
  28. package/dist/repo-resolver.js +211 -0
  29. package/dist/repo-resolver.js.map +1 -0
  30. package/dist/repo-uri-for-entry.d.ts +25 -0
  31. package/dist/repo-uri-for-entry.d.ts.map +1 -0
  32. package/dist/repo-uri-for-entry.js +64 -0
  33. package/dist/repo-uri-for-entry.js.map +1 -0
  34. package/dist/resources/repo-cluster.d.ts +19 -0
  35. package/dist/resources/repo-cluster.d.ts.map +1 -0
  36. package/dist/resources/repo-cluster.js +203 -0
  37. package/dist/resources/repo-cluster.js.map +1 -0
  38. package/dist/resources/repo-clusters.d.ts +14 -0
  39. package/dist/resources/repo-clusters.d.ts.map +1 -0
  40. package/dist/resources/repo-clusters.js +97 -0
  41. package/dist/resources/repo-clusters.js.map +1 -0
  42. package/dist/resources/repo-context.d.ts +12 -0
  43. package/dist/resources/repo-context.d.ts.map +1 -0
  44. package/dist/resources/repo-context.js +84 -0
  45. package/dist/resources/repo-context.js.map +1 -0
  46. package/dist/resources/repo-process.d.ts +19 -0
  47. package/dist/resources/repo-process.d.ts.map +1 -0
  48. package/dist/resources/repo-process.js +220 -0
  49. package/dist/resources/repo-process.js.map +1 -0
  50. package/dist/resources/repo-processes.d.ts +13 -0
  51. package/dist/resources/repo-processes.d.ts.map +1 -0
  52. package/dist/resources/repo-processes.js +99 -0
  53. package/dist/resources/repo-processes.js.map +1 -0
  54. package/dist/resources/repo-schema.d.ts +13 -0
  55. package/dist/resources/repo-schema.d.ts.map +1 -0
  56. package/dist/resources/repo-schema.js +99 -0
  57. package/dist/resources/repo-schema.js.map +1 -0
  58. package/dist/resources/repos.d.ts +20 -0
  59. package/dist/resources/repos.d.ts.map +1 -0
  60. package/dist/resources/repos.js +58 -0
  61. package/dist/resources/repos.js.map +1 -0
  62. package/dist/resources/store-helper.d.ts +28 -0
  63. package/dist/resources/store-helper.d.ts.map +1 -0
  64. package/dist/resources/store-helper.js +58 -0
  65. package/dist/resources/store-helper.js.map +1 -0
  66. package/dist/resources/yaml.d.ts +10 -0
  67. package/dist/resources/yaml.d.ts.map +1 -0
  68. package/dist/resources/yaml.js +16 -0
  69. package/dist/resources/yaml.js.map +1 -0
  70. package/dist/server.d.ts +46 -0
  71. package/dist/server.d.ts.map +1 -0
  72. package/dist/server.js +194 -0
  73. package/dist/server.js.map +1 -0
  74. package/dist/staleness.d.ts +19 -0
  75. package/dist/staleness.d.ts.map +1 -0
  76. package/dist/staleness.js +40 -0
  77. package/dist/staleness.js.map +1 -0
  78. package/dist/test-utils.d.ts +170 -0
  79. package/dist/test-utils.d.ts.map +1 -0
  80. package/dist/test-utils.js +473 -0
  81. package/dist/test-utils.js.map +1 -0
  82. package/dist/tools/api-impact.d.ts +47 -0
  83. package/dist/tools/api-impact.d.ts.map +1 -0
  84. package/dist/tools/api-impact.js +199 -0
  85. package/dist/tools/api-impact.js.map +1 -0
  86. package/dist/tools/confidence.d.ts +39 -0
  87. package/dist/tools/confidence.d.ts.map +1 -0
  88. package/dist/tools/confidence.js +58 -0
  89. package/dist/tools/confidence.js.map +1 -0
  90. package/dist/tools/context.d.ts +47 -0
  91. package/dist/tools/context.d.ts.map +1 -0
  92. package/dist/tools/context.js +577 -0
  93. package/dist/tools/context.js.map +1 -0
  94. package/dist/tools/dependencies.d.ts +29 -0
  95. package/dist/tools/dependencies.d.ts.map +1 -0
  96. package/dist/tools/dependencies.js +110 -0
  97. package/dist/tools/dependencies.js.map +1 -0
  98. package/dist/tools/detect-changes.d.ts +15 -0
  99. package/dist/tools/detect-changes.d.ts.map +1 -0
  100. package/dist/tools/detect-changes.js +78 -0
  101. package/dist/tools/detect-changes.js.map +1 -0
  102. package/dist/tools/group-contracts.d.ts +26 -0
  103. package/dist/tools/group-contracts.d.ts.map +1 -0
  104. package/dist/tools/group-contracts.js +251 -0
  105. package/dist/tools/group-contracts.js.map +1 -0
  106. package/dist/tools/group-cross-repo-links.d.ts +28 -0
  107. package/dist/tools/group-cross-repo-links.d.ts.map +1 -0
  108. package/dist/tools/group-cross-repo-links.js +128 -0
  109. package/dist/tools/group-cross-repo-links.js.map +1 -0
  110. package/dist/tools/group-list.d.ts +10 -0
  111. package/dist/tools/group-list.d.ts.map +1 -0
  112. package/dist/tools/group-list.js +74 -0
  113. package/dist/tools/group-list.js.map +1 -0
  114. package/dist/tools/group-query.d.ts +40 -0
  115. package/dist/tools/group-query.d.ts.map +1 -0
  116. package/dist/tools/group-query.js +209 -0
  117. package/dist/tools/group-query.js.map +1 -0
  118. package/dist/tools/group-status.d.ts +21 -0
  119. package/dist/tools/group-status.d.ts.map +1 -0
  120. package/dist/tools/group-status.js +121 -0
  121. package/dist/tools/group-status.js.map +1 -0
  122. package/dist/tools/group-sync.d.ts +23 -0
  123. package/dist/tools/group-sync.d.ts.map +1 -0
  124. package/dist/tools/group-sync.js +112 -0
  125. package/dist/tools/group-sync.js.map +1 -0
  126. package/dist/tools/impact.d.ts +36 -0
  127. package/dist/tools/impact.d.ts.map +1 -0
  128. package/dist/tools/impact.js +232 -0
  129. package/dist/tools/impact.js.map +1 -0
  130. package/dist/tools/license-audit.d.ts +34 -0
  131. package/dist/tools/license-audit.d.ts.map +1 -0
  132. package/dist/tools/license-audit.js +108 -0
  133. package/dist/tools/license-audit.js.map +1 -0
  134. package/dist/tools/list-dead-code.d.ts +26 -0
  135. package/dist/tools/list-dead-code.d.ts.map +1 -0
  136. package/dist/tools/list-dead-code.js +110 -0
  137. package/dist/tools/list-dead-code.js.map +1 -0
  138. package/dist/tools/list-findings-delta.d.ts +36 -0
  139. package/dist/tools/list-findings-delta.d.ts.map +1 -0
  140. package/dist/tools/list-findings-delta.js +274 -0
  141. package/dist/tools/list-findings-delta.js.map +1 -0
  142. package/dist/tools/list-findings.d.ts +30 -0
  143. package/dist/tools/list-findings.d.ts.map +1 -0
  144. package/dist/tools/list-findings.js +129 -0
  145. package/dist/tools/list-findings.js.map +1 -0
  146. package/dist/tools/list-repos.d.ts +17 -0
  147. package/dist/tools/list-repos.d.ts.map +1 -0
  148. package/dist/tools/list-repos.js +63 -0
  149. package/dist/tools/list-repos.js.map +1 -0
  150. package/dist/tools/owners.d.ts +23 -0
  151. package/dist/tools/owners.d.ts.map +1 -0
  152. package/dist/tools/owners.js +103 -0
  153. package/dist/tools/owners.js.map +1 -0
  154. package/dist/tools/pack-codebase.d.ts +76 -0
  155. package/dist/tools/pack-codebase.d.ts.map +1 -0
  156. package/dist/tools/pack-codebase.js +289 -0
  157. package/dist/tools/pack-codebase.js.map +1 -0
  158. package/dist/tools/project-profile.d.ts +28 -0
  159. package/dist/tools/project-profile.d.ts.map +1 -0
  160. package/dist/tools/project-profile.js +109 -0
  161. package/dist/tools/project-profile.js.map +1 -0
  162. package/dist/tools/query.d.ts +63 -0
  163. package/dist/tools/query.d.ts.map +1 -0
  164. package/dist/tools/query.js +662 -0
  165. package/dist/tools/query.js.map +1 -0
  166. package/dist/tools/remove-dead-code.d.ts +47 -0
  167. package/dist/tools/remove-dead-code.d.ts.map +1 -0
  168. package/dist/tools/remove-dead-code.js +258 -0
  169. package/dist/tools/remove-dead-code.js.map +1 -0
  170. package/dist/tools/rename.d.ts +21 -0
  171. package/dist/tools/rename.d.ts.map +1 -0
  172. package/dist/tools/rename.js +116 -0
  173. package/dist/tools/rename.js.map +1 -0
  174. package/dist/tools/risk-trends.d.ts +19 -0
  175. package/dist/tools/risk-trends.d.ts.map +1 -0
  176. package/dist/tools/risk-trends.js +73 -0
  177. package/dist/tools/risk-trends.js.map +1 -0
  178. package/dist/tools/route-map.d.ts +27 -0
  179. package/dist/tools/route-map.d.ts.map +1 -0
  180. package/dist/tools/route-map.js +119 -0
  181. package/dist/tools/route-map.js.map +1 -0
  182. package/dist/tools/scan.d.ts +27 -0
  183. package/dist/tools/scan.d.ts.map +1 -0
  184. package/dist/tools/scan.js +136 -0
  185. package/dist/tools/scan.js.map +1 -0
  186. package/dist/tools/shape-check.d.ts +53 -0
  187. package/dist/tools/shape-check.d.ts.map +1 -0
  188. package/dist/tools/shape-check.js +161 -0
  189. package/dist/tools/shape-check.js.map +1 -0
  190. package/dist/tools/shared.d.ts +101 -0
  191. package/dist/tools/shared.d.ts.map +1 -0
  192. package/dist/tools/shared.js +114 -0
  193. package/dist/tools/shared.js.map +1 -0
  194. package/dist/tools/signature.d.ts +38 -0
  195. package/dist/tools/signature.d.ts.map +1 -0
  196. package/dist/tools/signature.js +332 -0
  197. package/dist/tools/signature.js.map +1 -0
  198. package/dist/tools/sql.d.ts +34 -0
  199. package/dist/tools/sql.d.ts.map +1 -0
  200. package/dist/tools/sql.js +222 -0
  201. package/dist/tools/sql.js.map +1 -0
  202. package/dist/tools/tool-map.d.ts +24 -0
  203. package/dist/tools/tool-map.d.ts.map +1 -0
  204. package/dist/tools/tool-map.js +97 -0
  205. package/dist/tools/tool-map.js.map +1 -0
  206. package/dist/tools/verdict.d.ts +33 -0
  207. package/dist/tools/verdict.d.ts.map +1 -0
  208. package/dist/tools/verdict.js +102 -0
  209. package/dist/tools/verdict.js.map +1 -0
  210. package/package.json +76 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @opencodehub/mcp
2
+
3
+ Model Context Protocol server for OpenCodeHub. Wraps the analysis +
4
+ storage layer and exposes it to coding agents over stdio.
5
+
6
+ ## Surface
7
+
8
+ ```bash
9
+ codehub mcp # spawn the stdio server
10
+ ```
11
+
12
+ - Transport is stdio only — no HTTP, no SSE, no daemon
13
+ (`packages/cli/src/commands/mcp.ts`).
14
+ - `list_repos` is the discovery entry point. Per-repo tools accept an
15
+ optional `repo` (registry name) or `repo_uri` alias (Sourcegraph-style
16
+ URI like `github.com/org/repo`, `local:<hash>` for unpublished repos);
17
+ with one repo registered both are optional.
18
+ - When ≥ 2 repos are registered and neither is supplied, the tool
19
+ returns an `AMBIGUOUS_REPO` error envelope with `choices[]` (capped at
20
+ 10) so the caller can retry deterministically (see root `CLAUDE.md`).
21
+ - Every response carries a `next_steps` array and a
22
+ `_meta.codehub/staleness` entry when the index may be behind HEAD
23
+ (`packages/mcp/src/staleness.ts`).
24
+
25
+ ## Tools
26
+
27
+ 29 tools registered in `packages/mcp/src/server.ts:151-179`. Implementation
28
+ files live under `packages/mcp/src/tools/<id>.ts`.
29
+
30
+ | Group | Tools |
31
+ | ----------- | ---------------------------------------------------------------------------------------------------------- |
32
+ | Discovery | `list_repos`, `query`, `context`, `route_map`, `tool_map` |
33
+ | Impact | `impact`, `api_impact`, `detect_changes`, `shape_check`, `rename` |
34
+ | Snapshot | `pack_codebase`, `project_profile`, `dependencies`, `owners`, `risk_trends` |
35
+ | Findings | `scan`, `verdict`, `list_findings`, `list_findings_delta`, `license_audit` |
36
+ | Dead code | `list_dead_code`, `remove_dead_code` |
37
+ | Group | `group_list`, `group_query`, `group_status`, `group_contracts`, `group_cross_repo_links`, `group_sync` |
38
+ | Raw query | `sql` |
39
+
40
+ ## Design
41
+
42
+ - **Single source of truth** — registration order in `server.ts` IS the
43
+ surface. `tool_map` introspects the live server so agents can list
44
+ tools without out-of-band documentation
45
+ (`packages/mcp/src/tools/tool-map.ts`).
46
+ - **Structured errors over prose** — every error returns
47
+ `structuredContent.error = { error_code, jsonrpc_code, ... }` so a
48
+ caller can branch on `error_code` instead of regex-matching
49
+ (`packages/mcp/src/error-envelope.ts`).
50
+ - **Repo resolution is centralised** — `repoResolver` and the
51
+ AMBIGUOUS_REPO envelope are wired through every per-repo tool so
52
+ ambiguity is reported once, consistently
53
+ (`packages/mcp/src/repo-resolver.ts`).
54
+ - **Connection pooling** — the graph store is held in a per-process
55
+ pool to amortise DuckDB cold starts across many tool calls
56
+ (`packages/mcp/src/connection-pool.ts`).
57
+ - **Lazy analysis** — heavy work (scan, code-pack, verdict) shells out
58
+ via `analysis-bridge` rather than running in the MCP process so a
59
+ hung scanner cannot stall the server (`packages/mcp/src/analysis-bridge.ts`).
60
+
61
+ See ADR 0012 for the `repo_uri`-as-typed-attribute rationale and the
62
+ root `CLAUDE.md` for the AMBIGUOUS_REPO retry contract.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Bridge to `@opencodehub/analysis`.
3
+ *
4
+ * The analysis package exposes `runImpact`, `runRename`, `runDetectChanges`,
5
+ * and `computeStaleness`, which this bridge re-exports for the tool handlers.
6
+ * A slim inline-impact fallback remains as a safety net for repos where
7
+ * analysis cannot resolve the target — e.g. a bare node-id with no
8
+ * declaration row — so the `impact` tool always returns something actionable.
9
+ */
10
+ import { type DetectChangesQuery, type DetectChangesResult, type FsAbstraction, type ImpactQuery, type ImpactResult, type RenameQuery, type RenameResult } from "@opencodehub/analysis";
11
+ import type { IGraphStore } from "@opencodehub/storage";
12
+ export type { DetectChangesQuery, DetectChangesResult, ImpactQuery, ImpactResult, RenameEdit, RenameQuery, RenameResult, } from "@opencodehub/analysis";
13
+ export declare function callRunImpact(store: IGraphStore, q: ImpactQuery): Promise<ImpactResult>;
14
+ export declare function callRunRename(store: IGraphStore, q: RenameQuery, repoRoot: string, fs?: FsAbstraction): Promise<RenameResult>;
15
+ export declare function callRunDetectChanges(store: IGraphStore, q: DetectChangesQuery): Promise<DetectChangesResult>;
16
+ /**
17
+ * Graph-only impact fallback. Produces the same `ImpactResult` shape as
18
+ * the analysis package but skips the name-to-id resolution step — useful
19
+ * when the target is already a node id and the caller does not need
20
+ * candidate disambiguation.
21
+ */
22
+ export declare function inlineImpact(store: IGraphStore, q: ImpactQuery): Promise<ImpactResult>;
23
+ //# sourceMappingURL=analysis-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-bridge.d.ts","sourceRoot":"","sources":["../src/analysis-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAKL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,uBAAuB,CAAC;AAE/B,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAE7F;AAED,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,CAAC,EAAE,WAAW,EACd,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,aAAa,GACjB,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,WAAW,EAClB,CAAC,EAAE,kBAAkB,GACpB,OAAO,CAAC,mBAAmB,CAAC,CAE9B;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAgE5F"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Bridge to `@opencodehub/analysis`.
3
+ *
4
+ * The analysis package exposes `runImpact`, `runRename`, `runDetectChanges`,
5
+ * and `computeStaleness`, which this bridge re-exports for the tool handlers.
6
+ * A slim inline-impact fallback remains as a safety net for repos where
7
+ * analysis cannot resolve the target — e.g. a bare node-id with no
8
+ * declaration row — so the `impact` tool always returns something actionable.
9
+ */
10
+ import { runDetectChanges as analysisRunDetectChanges, runImpact as analysisRunImpact, runRename as analysisRunRename, createNodeFs, } from "@opencodehub/analysis";
11
+ export async function callRunImpact(store, q) {
12
+ return analysisRunImpact(store, q);
13
+ }
14
+ export async function callRunRename(store, q, repoRoot, fs) {
15
+ return analysisRunRename(store, q, fs ?? createNodeFs(), repoRoot);
16
+ }
17
+ export async function callRunDetectChanges(store, q) {
18
+ return analysisRunDetectChanges(store, q);
19
+ }
20
+ /**
21
+ * Graph-only impact fallback. Produces the same `ImpactResult` shape as
22
+ * the analysis package but skips the name-to-id resolution step — useful
23
+ * when the target is already a node id and the caller does not need
24
+ * candidate disambiguation.
25
+ */
26
+ export async function inlineImpact(store, q) {
27
+ const maxDepth = q.maxDepth ?? 3;
28
+ const minConfidence = q.minConfidence ?? 0.3;
29
+ const direction = q.direction === "upstream" ? "up" : q.direction === "downstream" ? "down" : "both";
30
+ const travArgs = {
31
+ startId: q.target,
32
+ direction,
33
+ maxDepth,
34
+ minConfidence,
35
+ };
36
+ if (q.relationTypes && q.relationTypes.length > 0) {
37
+ travArgs.relationTypes = q.relationTypes;
38
+ }
39
+ const results = await store.traverse(travArgs);
40
+ const byDepthMap = new Map();
41
+ for (const r of results) {
42
+ let bucket = byDepthMap.get(r.depth);
43
+ if (!bucket) {
44
+ bucket = new Map();
45
+ byDepthMap.set(r.depth, bucket);
46
+ }
47
+ bucket.set(r.nodeId, true);
48
+ }
49
+ const depths = Array.from(byDepthMap.keys()).sort((a, b) => a - b);
50
+ const byDepth = depths.map((depth) => {
51
+ const bucket = byDepthMap.get(depth);
52
+ const ids = bucket ? Array.from(bucket.keys()) : [];
53
+ return {
54
+ depth,
55
+ nodes: ids.map((nodeId) => ({
56
+ id: nodeId,
57
+ name: nodeId,
58
+ filePath: "",
59
+ kind: "",
60
+ viaRelation: "CALLS",
61
+ })),
62
+ };
63
+ });
64
+ const d1 = byDepthMap.get(1)?.size ?? 0;
65
+ let risk = "LOW";
66
+ if (d1 >= 20)
67
+ risk = "CRITICAL";
68
+ else if (d1 >= 8)
69
+ risk = "HIGH";
70
+ else if (d1 >= 3)
71
+ risk = "MEDIUM";
72
+ return {
73
+ targetCandidates: [],
74
+ byDepth,
75
+ risk,
76
+ totalAffected: results.length,
77
+ ambiguous: false,
78
+ affectedProcesses: [],
79
+ affectedModules: [],
80
+ traversedEdges: [],
81
+ };
82
+ }
83
+ //# sourceMappingURL=analysis-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-bridge.js","sourceRoot":"","sources":["../src/analysis-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * LRU-backed connection pool for graph stores.
3
+ *
4
+ * A single MCP session routinely fields back-to-back tool calls that all
5
+ * target the same repo; opening the underlying database for every call
6
+ * would be wasteful. We cache open `Store` (= `OpenStoreResult`) handles
7
+ * keyed by absolute repo path, with three safety guards on top of a plain
8
+ * LRU:
9
+ *
10
+ * 1. Per-key promise dedupe. Concurrent acquires for the same repo share
11
+ * a single in-flight open() — otherwise DuckDB will raise on the
12
+ * second connection opening the same file in read-write mode.
13
+ * 2. Reference counting. Release must decrement a per-entry counter; an
14
+ * eviction that lands on a still-in-use entry MUST NOT close it. We
15
+ * mark it `closed` deferred and the last release actually closes.
16
+ * 3. Idle TTL. lru-cache@11 bumps recency on every acquire, so a repo
17
+ * that is actively queried never evicts; an idle repo closes after
18
+ * 15 minutes.
19
+ *
20
+ * `shutdown()` drains the pool on stdio close so the server exits cleanly.
21
+ *
22
+ * The pool caches the composed `OpenStoreResult` so MCP tools can route
23
+ * graph-tier calls through `store.graph` and temporal-tier calls
24
+ * (cochanges, summaries, `--sql` escape hatch) through `store.temporal`.
25
+ * Backend selection follows the standard `openStore` resolution (env-
26
+ * driven `CODEHUB_STORE`, with auto-detect when unset).
27
+ * `OpenStoreResult.close()` is the deterministic composite close — for
28
+ * the DuckDB-only deployment that's a single underlying close.
29
+ */
30
+ import { type Store } from "@opencodehub/storage";
31
+ export interface PoolEntry {
32
+ readonly store: Store;
33
+ refCount: number;
34
+ closed: boolean;
35
+ /** Set when an eviction fires while refCount > 0; close on last release. */
36
+ closePending: boolean;
37
+ }
38
+ export interface ConnectionPoolOptions {
39
+ readonly max?: number;
40
+ readonly ttlMs?: number;
41
+ }
42
+ /**
43
+ * Factory indirection keeps tests mockable without standing up the
44
+ * underlying database. Production always calls `openStore` so backend
45
+ * selection (DuckDB or the graph-db pairing) follows the env-driven
46
+ * resolution.
47
+ */
48
+ export type StoreFactory = (dbPath: string) => Promise<Store>;
49
+ export declare class ConnectionPool {
50
+ private readonly cache;
51
+ private readonly inflight;
52
+ private readonly factory;
53
+ private disposed;
54
+ constructor(opts?: ConnectionPoolOptions, factory?: StoreFactory);
55
+ /**
56
+ * Acquire a store handle for the given repo. The caller MUST pair every
57
+ * acquire with a release. The `dbPath` argument is the absolute path to
58
+ * the on-disk DuckDB file; `repoKey` is a stable identifier used for
59
+ * caching (usually the absolute repo path).
60
+ */
61
+ acquire(repoKey: string, dbPath: string): Promise<Store>;
62
+ /**
63
+ * Release a previously-acquired handle. If the entry was evicted while
64
+ * in use (`closePending`), the last release closes the store.
65
+ */
66
+ release(repoKey: string): Promise<void>;
67
+ /**
68
+ * Drain the pool: wait on any inflight opens, then close every cached
69
+ * entry. Safe to call multiple times.
70
+ */
71
+ shutdown(): Promise<void>;
72
+ /** Test-only view of cached keys; stable iteration order is not guaranteed. */
73
+ size(): number;
74
+ private findEvicted;
75
+ }
76
+ //# sourceMappingURL=connection-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-pool.d.ts","sourceRoot":"","sources":["../src/connection-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAa,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG7D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAKD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;AAiB9D,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAS;gBAEb,IAAI,GAAE,qBAA0B,EAAE,OAAO,GAAE,YAA6B;IA0BpF;;;;;OAKG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAsC9D;;;OAGG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY7C;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB/B,+EAA+E;IAC/E,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,WAAW;CAYpB"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * LRU-backed connection pool for graph stores.
3
+ *
4
+ * A single MCP session routinely fields back-to-back tool calls that all
5
+ * target the same repo; opening the underlying database for every call
6
+ * would be wasteful. We cache open `Store` (= `OpenStoreResult`) handles
7
+ * keyed by absolute repo path, with three safety guards on top of a plain
8
+ * LRU:
9
+ *
10
+ * 1. Per-key promise dedupe. Concurrent acquires for the same repo share
11
+ * a single in-flight open() — otherwise DuckDB will raise on the
12
+ * second connection opening the same file in read-write mode.
13
+ * 2. Reference counting. Release must decrement a per-entry counter; an
14
+ * eviction that lands on a still-in-use entry MUST NOT close it. We
15
+ * mark it `closed` deferred and the last release actually closes.
16
+ * 3. Idle TTL. lru-cache@11 bumps recency on every acquire, so a repo
17
+ * that is actively queried never evicts; an idle repo closes after
18
+ * 15 minutes.
19
+ *
20
+ * `shutdown()` drains the pool on stdio close so the server exits cleanly.
21
+ *
22
+ * The pool caches the composed `OpenStoreResult` so MCP tools can route
23
+ * graph-tier calls through `store.graph` and temporal-tier calls
24
+ * (cochanges, summaries, `--sql` escape hatch) through `store.temporal`.
25
+ * Backend selection follows the standard `openStore` resolution (env-
26
+ * driven `CODEHUB_STORE`, with auto-detect when unset).
27
+ * `OpenStoreResult.close()` is the deterministic composite close — for
28
+ * the DuckDB-only deployment that's a single underlying close.
29
+ */
30
+ import { openStore } from "@opencodehub/storage";
31
+ import { LRUCache } from "lru-cache";
32
+ const DEFAULT_MAX = 8;
33
+ const DEFAULT_TTL_MS = 15 * 60 * 1000;
34
+ const defaultFactory = async (dbPath) => {
35
+ // openStore picks backend via CODEHUB_STORE (defaults to "duck"). We
36
+ // open read-only because every MCP tool is a reader; the ingestion
37
+ // pipeline owns writes and runs out-of-process.
38
+ const store = await openStore({ path: dbPath, readOnly: true });
39
+ await store.graph.open();
40
+ if (store.graphFile !== store.temporalFile) {
41
+ // Two distinct underlying files — open each side. For the default
42
+ // DuckDB backend graph and temporal alias the same instance and the
43
+ // second open() is a no-op.
44
+ await store.temporal.open();
45
+ }
46
+ return store;
47
+ };
48
+ export class ConnectionPool {
49
+ cache;
50
+ inflight = new Map();
51
+ factory;
52
+ disposed = false;
53
+ constructor(opts = {}, factory = defaultFactory) {
54
+ this.factory = factory;
55
+ this.cache = new LRUCache({
56
+ max: opts.max ?? DEFAULT_MAX,
57
+ ttl: opts.ttlMs ?? DEFAULT_TTL_MS,
58
+ updateAgeOnGet: true,
59
+ // The dispose callback fires on eviction (size or ttl) and on
60
+ // cache.clear(). We treat eviction as "this entry is no longer
61
+ // reachable from the cache"; if nobody is using it we close now,
62
+ // otherwise we let the last `release()` close it.
63
+ dispose: (entry, key) => {
64
+ if (entry.closed)
65
+ return;
66
+ if (entry.refCount === 0) {
67
+ entry.closed = true;
68
+ void entry.store.close().catch(() => {
69
+ /* swallow — best effort during eviction */
70
+ });
71
+ }
72
+ else {
73
+ entry.closePending = true;
74
+ }
75
+ // Ensure we don't leak a stale inflight promise for an evicted key.
76
+ this.inflight.delete(key);
77
+ },
78
+ });
79
+ }
80
+ /**
81
+ * Acquire a store handle for the given repo. The caller MUST pair every
82
+ * acquire with a release. The `dbPath` argument is the absolute path to
83
+ * the on-disk DuckDB file; `repoKey` is a stable identifier used for
84
+ * caching (usually the absolute repo path).
85
+ */
86
+ async acquire(repoKey, dbPath) {
87
+ if (this.disposed) {
88
+ throw new Error("ConnectionPool is shut down");
89
+ }
90
+ const existing = this.cache.get(repoKey);
91
+ if (existing && !existing.closed) {
92
+ existing.refCount += 1;
93
+ return existing.store;
94
+ }
95
+ const pending = this.inflight.get(repoKey);
96
+ if (pending) {
97
+ const entry = await pending;
98
+ entry.refCount += 1;
99
+ return entry.store;
100
+ }
101
+ const promise = (async () => {
102
+ const store = await this.factory(dbPath);
103
+ const entry = {
104
+ store,
105
+ refCount: 0,
106
+ closed: false,
107
+ closePending: false,
108
+ };
109
+ this.cache.set(repoKey, entry);
110
+ return entry;
111
+ })();
112
+ this.inflight.set(repoKey, promise);
113
+ try {
114
+ const entry = await promise;
115
+ entry.refCount += 1;
116
+ return entry.store;
117
+ }
118
+ finally {
119
+ this.inflight.delete(repoKey);
120
+ }
121
+ }
122
+ /**
123
+ * Release a previously-acquired handle. If the entry was evicted while
124
+ * in use (`closePending`), the last release closes the store.
125
+ */
126
+ async release(repoKey) {
127
+ const entry = this.cache.peek(repoKey) ?? this.findEvicted(repoKey);
128
+ if (!entry)
129
+ return;
130
+ if (entry.refCount > 0)
131
+ entry.refCount -= 1;
132
+ if (entry.refCount === 0 && entry.closePending && !entry.closed) {
133
+ entry.closed = true;
134
+ await entry.store.close().catch(() => {
135
+ /* swallow */
136
+ });
137
+ }
138
+ }
139
+ /**
140
+ * Drain the pool: wait on any inflight opens, then close every cached
141
+ * entry. Safe to call multiple times.
142
+ */
143
+ async shutdown() {
144
+ if (this.disposed)
145
+ return;
146
+ this.disposed = true;
147
+ // Wait for inflight opens so we don't leak half-opened DBs.
148
+ const pending = Array.from(this.inflight.values());
149
+ await Promise.allSettled(pending);
150
+ this.inflight.clear();
151
+ const entries = [];
152
+ for (const entry of this.cache.values())
153
+ entries.push(entry);
154
+ this.cache.clear(); // triggers dispose on remaining entries
155
+ await Promise.allSettled(entries.map(async (entry) => {
156
+ if (!entry.closed) {
157
+ entry.closed = true;
158
+ await entry.store.close();
159
+ }
160
+ }));
161
+ }
162
+ /** Test-only view of cached keys; stable iteration order is not guaranteed. */
163
+ size() {
164
+ return this.cache.size;
165
+ }
166
+ findEvicted(_repoKey) {
167
+ // After dispose runs, the entry is gone from the cache; the caller
168
+ // holds no direct reference to it here. We intentionally don't store
169
+ // a secondary map — reference counting for evicted entries is tracked
170
+ // inside the entry object itself, which remains reachable via the
171
+ // store reference that the tool handler still holds. For the current
172
+ // MVP usage (single-threaded tool handlers that acquire + release in
173
+ // the same function) this branch is unreachable, so we return
174
+ // undefined and rely on the dispose path to have already closed the
175
+ // store if refCount was 0.
176
+ return undefined;
177
+ }
178
+ }
179
+ //# sourceMappingURL=connection-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-pool.js","sourceRoot":"","sources":["../src/connection-pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Uniform error envelope for MCP tool responses.
3
+ *
4
+ * The OpenCodeHub PRD (§9.1) defines a single error code enumeration used
5
+ * across the MCP surface. Every tool that fails gracefully (i.e. the tool
6
+ * ran but the operation could not complete) returns this shape so agents
7
+ * can key on `error.code` to decide whether to retry, disambiguate, or
8
+ * abort.
9
+ *
10
+ * For protocol-level failures (unknown tool name, malformed JSON-RPC) the
11
+ * SDK's `McpError` class is thrown instead — those do not go through this
12
+ * helper.
13
+ */
14
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
15
+ /** The fixed set of tool-level error codes exposed to MCP clients. */
16
+ export type ErrorCode = "STALENESS" | "INVALID_INPUT" | "NOT_FOUND" | "DB_ERROR" | "SCHEMA_MISMATCH" | "RATE_LIMITED" | "INTERNAL" | "NO_INDEX" | "AMBIGUOUS_REPO" | "EMBEDDER_MISMATCH";
17
+ /** Structured shape carried under `structuredContent.error`. */
18
+ export interface ErrorDetail {
19
+ readonly code: ErrorCode;
20
+ readonly message: string;
21
+ readonly hint?: string;
22
+ }
23
+ /**
24
+ * One registered repo exposed to the caller in an `AMBIGUOUS_REPO` envelope
25
+ * so the LLM can retry with an explicit `repo_uri`. Snake-case wire fields
26
+ * are intentional — this shape crosses the MCP boundary to an agent, and
27
+ * the research spec (§6.2 of research-m5m6.yaml) names them that way.
28
+ *
29
+ * `repo_uri` is derived from the registry at error-construction time.
30
+ * Once the registry surfaces the persisted RepoNode, this field will
31
+ * be pulled from there instead of being computed from
32
+ * `RegistryEntry.name`.
33
+ */
34
+ export interface RepoChoice {
35
+ readonly repo_uri: string;
36
+ readonly default_branch: string | null;
37
+ readonly group: string | null;
38
+ }
39
+ /**
40
+ * Extended detail shape for `AMBIGUOUS_REPO`. Retains the legacy
41
+ * `{ code, message, hint }` surface so existing callers (and tests at
42
+ * error-envelope.test.ts:39-47) keep working; adds structured fields for
43
+ * LLM disambiguation.
44
+ */
45
+ export interface AmbiguousRepoDetail extends ErrorDetail {
46
+ readonly code: "AMBIGUOUS_REPO";
47
+ /** Alias of `code` — matches the `error_code` field in the research spec. */
48
+ readonly error_code: "AMBIGUOUS_REPO";
49
+ /** JSON-RPC code for "invalid params" — per MCP spec. */
50
+ readonly jsonrpc_code: -32602;
51
+ /** Capped at 10. */
52
+ readonly choices: readonly RepoChoice[];
53
+ /** Full count of matching registry entries (may exceed `choices.length`). */
54
+ readonly total_matches: number;
55
+ }
56
+ /**
57
+ * Input to {@link toolAmbiguousRepoError}. Caller (typically the repo
58
+ * resolver at `repo-resolver.ts`) provides the full choice set; this
59
+ * builder caps it to 10 and reports the untruncated total.
60
+ */
61
+ export interface AmbiguousRepoPayload {
62
+ readonly message: string;
63
+ readonly hint: string;
64
+ readonly choices: readonly RepoChoice[];
65
+ readonly totalMatches: number;
66
+ }
67
+ /**
68
+ * Build a tool-level error result. Both `content` (for clients that only
69
+ * read text) and `structuredContent` (for clients that honour the output
70
+ * schema) are populated, and `isError` is set so output-schema validation
71
+ * is skipped by the SDK per the 2025-06-18 spec revision.
72
+ */
73
+ export declare function toolError(code: ErrorCode, message: string, hint?: string): CallToolResult;
74
+ /**
75
+ * Map an arbitrary thrown value to an `INTERNAL` error envelope. Used as a
76
+ * catch-all at the boundary of each tool handler so unexpected exceptions
77
+ * reach the agent as a structured error instead of tearing down the stdio
78
+ * transport.
79
+ */
80
+ export declare function toolErrorFromUnknown(err: unknown, hint?: string): CallToolResult;
81
+ /**
82
+ * Max number of `choices[]` entries carried in an AMBIGUOUS_REPO envelope.
83
+ * More than 10 gets truncated; `total_matches` still reports the full count
84
+ * so the caller knows there is more.
85
+ */
86
+ export declare const AMBIGUOUS_REPO_CHOICES_CAP = 10;
87
+ /**
88
+ * Build a structured AMBIGUOUS_REPO envelope. Wraps {@link toolError} so
89
+ * the legacy `{ code, message, hint }` fields stay intact (back-compat with
90
+ * `error-envelope.test.ts:39-47`) and layers on `error_code`, `choices[]`,
91
+ * `total_matches` for disambiguation by an agent.
92
+ *
93
+ * Choices are capped at {@link AMBIGUOUS_REPO_CHOICES_CAP}; `total_matches`
94
+ * always reports the pre-truncation count.
95
+ */
96
+ export declare function toolAmbiguousRepoError(payload: AmbiguousRepoPayload): CallToolResult;
97
+ //# sourceMappingURL=error-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-envelope.d.ts","sourceRoot":"","sources":["../src/error-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,sEAAsE;AACtE,MAAM,MAAM,SAAS,GACjB,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,iBAAiB,GACjB,cAAc,GACd,UAAU,GACV,UAAU,GACV,gBAAgB,GAChB,mBAAmB,CAAC;AAExB,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,yDAAyD;IACzD,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC;IAC9B,oBAAoB;IACpB,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,CASzF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,CAGhF;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,cAAc,CAkBpF"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Uniform error envelope for MCP tool responses.
3
+ *
4
+ * The OpenCodeHub PRD (§9.1) defines a single error code enumeration used
5
+ * across the MCP surface. Every tool that fails gracefully (i.e. the tool
6
+ * ran but the operation could not complete) returns this shape so agents
7
+ * can key on `error.code` to decide whether to retry, disambiguate, or
8
+ * abort.
9
+ *
10
+ * For protocol-level failures (unknown tool name, malformed JSON-RPC) the
11
+ * SDK's `McpError` class is thrown instead — those do not go through this
12
+ * helper.
13
+ */
14
+ /**
15
+ * Build a tool-level error result. Both `content` (for clients that only
16
+ * read text) and `structuredContent` (for clients that honour the output
17
+ * schema) are populated, and `isError` is set so output-schema validation
18
+ * is skipped by the SDK per the 2025-06-18 spec revision.
19
+ */
20
+ export function toolError(code, message, hint) {
21
+ const lines = [`Error (${code}): ${message}`];
22
+ if (hint)
23
+ lines.push(`Hint: ${hint}`);
24
+ const detail = hint ? { code, message, hint } : { code, message };
25
+ return {
26
+ content: [{ type: "text", text: lines.join("\n") }],
27
+ structuredContent: { error: detail },
28
+ isError: true,
29
+ };
30
+ }
31
+ /**
32
+ * Map an arbitrary thrown value to an `INTERNAL` error envelope. Used as a
33
+ * catch-all at the boundary of each tool handler so unexpected exceptions
34
+ * reach the agent as a structured error instead of tearing down the stdio
35
+ * transport.
36
+ */
37
+ export function toolErrorFromUnknown(err, hint) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ return toolError("INTERNAL", message, hint);
40
+ }
41
+ /**
42
+ * Max number of `choices[]` entries carried in an AMBIGUOUS_REPO envelope.
43
+ * More than 10 gets truncated; `total_matches` still reports the full count
44
+ * so the caller knows there is more.
45
+ */
46
+ export const AMBIGUOUS_REPO_CHOICES_CAP = 10;
47
+ /**
48
+ * Build a structured AMBIGUOUS_REPO envelope. Wraps {@link toolError} so
49
+ * the legacy `{ code, message, hint }` fields stay intact (back-compat with
50
+ * `error-envelope.test.ts:39-47`) and layers on `error_code`, `choices[]`,
51
+ * `total_matches` for disambiguation by an agent.
52
+ *
53
+ * Choices are capped at {@link AMBIGUOUS_REPO_CHOICES_CAP}; `total_matches`
54
+ * always reports the pre-truncation count.
55
+ */
56
+ export function toolAmbiguousRepoError(payload) {
57
+ const capped = payload.choices.slice(0, AMBIGUOUS_REPO_CHOICES_CAP);
58
+ const base = toolError("AMBIGUOUS_REPO", payload.message, payload.hint);
59
+ const baseDetail = base.structuredContent.error;
60
+ const detail = {
61
+ code: "AMBIGUOUS_REPO",
62
+ message: baseDetail.message,
63
+ ...(baseDetail.hint !== undefined ? { hint: baseDetail.hint } : {}),
64
+ error_code: "AMBIGUOUS_REPO",
65
+ jsonrpc_code: -32602,
66
+ choices: capped,
67
+ total_matches: payload.totalMatches,
68
+ };
69
+ return {
70
+ content: base.content,
71
+ structuredContent: { error: detail },
72
+ isError: true,
73
+ };
74
+ }
75
+ //# sourceMappingURL=error-envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-envelope.js","sourceRoot":"","sources":["../src/error-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAuEH;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,OAAe,EAAE,IAAa;IACvE,MAAM,KAAK,GAAG,CAAC,UAAU,IAAI,MAAM,OAAO,EAAE,CAAC,CAAC;IAC9C,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,MAAM,GAAgB,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC/E,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5D,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;QACpC,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAY,EAAE,IAAa;IAC9D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,SAAS,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAA6B;IAClE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,0BAA0B,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,SAAS,CAAC,gBAAgB,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,MAAM,UAAU,GAAI,IAAI,CAAC,iBAA4C,CAAC,KAAK,CAAC;IAC5E,MAAM,MAAM,GAAwB;QAClC,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,UAAU,EAAE,gBAAgB;QAC5B,YAAY,EAAE,CAAC,KAAK;QACpB,OAAO,EAAE,MAAM;QACf,aAAa,EAAE,OAAO,CAAC,YAAY;KACpC,CAAC;IACF,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;QACpC,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC"}