@selfagency/beans-mcp 0.1.3 → 0.4.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 (59) hide show
  1. package/README.md +63 -6
  2. package/{dist/beans-mcp-server.cjs → beans-mcp-server.cjs} +269 -34
  3. package/{dist/index.cjs → index.cjs} +269 -34
  4. package/{dist/index.d.ts → index.d.ts} +19 -1
  5. package/{dist/index.js → index.js} +269 -34
  6. package/package.json +28 -64
  7. package/.beans.yml +0 -6
  8. package/.claude/settings.local.json +0 -18
  9. package/.editorconfig +0 -13
  10. package/.github/dependabot.yml +0 -11
  11. package/.github/workflows/release.yml +0 -235
  12. package/.github/workflows/test.yml +0 -84
  13. package/.husky/pre-commit +0 -1
  14. package/.nvmrc +0 -1
  15. package/.oxfmtrc.json +0 -11
  16. package/.oxlintrc.json +0 -37
  17. package/.vscode/settings.json +0 -3
  18. package/CHANGELOG.md +0 -160
  19. package/CONTRIBUTING.md +0 -139
  20. package/LICENSE.txt +0 -21
  21. package/codeql/codeql-custom-queries-actions/README.md +0 -14
  22. package/codeql/codeql-custom-queries-actions/codeql-pack.lock.yml +0 -32
  23. package/codeql/codeql-custom-queries-actions/codeql-pack.yml +0 -7
  24. package/codeql/codeql-custom-queries-actions/qlpack.yml +0 -6
  25. package/codeql/codeql-custom-queries-actions/queries/github-script-without-tojson.ql +0 -18
  26. package/codeql/codeql-custom-queries-actions/queries/strict-external-action-pinning.ql +0 -18
  27. package/codeql/codeql-custom-queries-javascript/README.md +0 -14
  28. package/codeql/codeql-custom-queries-javascript/codeql-pack.lock.yml +0 -30
  29. package/codeql/codeql-custom-queries-javascript/codeql-pack.yml +0 -7
  30. package/codeql/codeql-custom-queries-javascript/qlpack.yml +0 -6
  31. package/codeql/codeql-custom-queries-javascript/queries/child-process-shell-apis.ql +0 -26
  32. package/codeql/codeql-custom-queries-javascript/queries/innerhtml-assignment.ql +0 -24
  33. package/dist/README.md +0 -307
  34. package/dist/beans-mcp-server.cjs.map +0 -1
  35. package/dist/index.cjs.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/package.json +0 -43
  38. package/pnpm-workspace.yaml +0 -2
  39. package/scripts/release.js +0 -433
  40. package/scripts/write-dist-package.js +0 -53
  41. package/src/cli.ts +0 -14
  42. package/src/index.ts +0 -21
  43. package/src/internal/graphql.ts +0 -33
  44. package/src/internal/queryHelpers.ts +0 -157
  45. package/src/server/BeansMcpServer.ts +0 -623
  46. package/src/server/backend.ts +0 -364
  47. package/src/test/BeansMcpServer.test.ts +0 -514
  48. package/src/test/handlers.unit.test.ts +0 -201
  49. package/src/test/parseCliArgs.test.ts +0 -69
  50. package/src/test/protocol.e2e.test.ts +0 -884
  51. package/src/test/queryHelpers.test.ts +0 -524
  52. package/src/test/startBeansMcpServer.test.ts +0 -146
  53. package/src/test/tools-integration.test.ts +0 -912
  54. package/src/test/utils.test.ts +0 -81
  55. package/src/types.ts +0 -46
  56. package/src/utils.ts +0 -20
  57. package/tsconfig.json +0 -24
  58. package/tsup.config.ts +0 -42
  59. package/vitest.config.ts +0 -18
package/README.md CHANGED
@@ -12,6 +12,14 @@ MCP (Model Context Protocol) server for [Beans](https://github.com/hmans/beans)
12
12
  npx @selfagency/beans-mcp /path/to/workspace
13
13
  ```
14
14
 
15
+ ### Versioning
16
+
17
+ `@selfagency/beans-mcp` tracks upstream [Beans](https://github.com/hmans/beans) versions.
18
+ For example, Beans `v0.4.2` maps to `@selfagency/beans-mcp@0.4.2`.
19
+
20
+ At startup, the server compares its own package version against the installed `beans`
21
+ CLI version. If they differ, it prints a warning to stderr and continues startup.
22
+
15
23
  ### Parameters
16
24
 
17
25
  - `--workspace-root` or positional arg: Workspace root path
@@ -23,12 +31,12 @@ npx @selfagency/beans-mcp /path/to/workspace
23
31
  ## Summary of public MCP tools
24
32
 
25
33
  - `beans_init` — Initialize the workspace (optional `prefix`).
26
- - `beans_view` — Fetch full bean details by `beanId`.
34
+ - `beans_view` — Fetch full bean details by `beanId` or `beanIds`.
27
35
  - `beans_create` — Create a new bean (title/type + optional fields).
28
- - `beans_update` — Consolidated metadata updates (status/type/priority/parent/clearParent/blocking/blockedBy).
29
- - `beans_delete` — Delete a bean (`beanId`, optional `force`).
36
+ - `beans_update` — Consolidated metadata + body updates (status/type/priority/parent/clearParent/blocking/blockedBy/body/bodyAppend/bodyReplace) plus optional optimistic concurrency hint (`ifMatch`).
37
+ - `beans_delete` — Delete one or many beans (`beanId` or `beanIds`, optional `force`).
30
38
  - `beans_reopen` — Reopen a completed or scrapped bean to an active status.
31
- - `beans_query` — Unified list/search/filter/sort/llm_context/open_config operations.
39
+ - `beans_query` — Unified list/search/filter/sort/ready/llm_context/open_config operations.
32
40
  - `beans_bean_file` — Read/edit/create/delete files under `.beans`.
33
41
  - `beans_output` — Read extension output logs or show guidance.
34
42
 
@@ -37,6 +45,7 @@ npx @selfagency/beans-mcp /path/to/workspace
37
45
  - The `beans_query` tool is intentionally broad: prefer it for listing, searching, filtering or sorting beans, and for generating Copilot instructions (`operation: 'llm_context'`).
38
46
  - All file and log operations validate paths to keep them within the workspace or the VS Code log directory.
39
47
  - `beans_update` replaces many fine-grained update tools; callers should use it to keep the public tool surface small and predictable.
48
+ - Version mismatches are warning-only and non-blocking by design.
40
49
 
41
50
  ## Examples
42
51
 
@@ -62,6 +71,12 @@ Request:
62
71
  { "beanId": "bean-abc" }
63
72
  ```
64
73
 
74
+ Request (multiple beans):
75
+
76
+ ```json
77
+ { "beanIds": ["bean-abc", "bean-def"] }
78
+ ```
79
+
65
80
  Response (structuredContent):
66
81
 
67
82
  ```json
@@ -114,10 +129,26 @@ Request (change status and add blocking):
114
129
  {
115
130
  "beanId": "bean-abc",
116
131
  "status": "in-progress",
117
- "blocking": ["bean-def"]
132
+ "blocking": ["bean-def"],
133
+ "ifMatch": "etag-value"
118
134
  }
119
135
  ```
120
136
 
137
+ Request (atomic body modifications):
138
+
139
+ ```json
140
+ {
141
+ "beanId": "bean-abc",
142
+ "bodyReplace": [
143
+ { "old": "- [ ] Task 1", "new": "- [x] Task 1" },
144
+ { "old": "- [ ] Task 2", "new": "- [x] Task 2" }
145
+ ],
146
+ "bodyAppend": "## Summary\n\nAll checklist items completed."
147
+ }
148
+ ```
149
+
150
+ > Note: `body` (full replacement) cannot be combined with `bodyAppend` or `bodyReplace` in the same request.
151
+
121
152
  Response (structuredContent):
122
153
 
123
154
  ```json
@@ -144,6 +175,26 @@ Response:
144
175
  { "deleted": true, "beanId": "bean-old" }
145
176
  ```
146
177
 
178
+ Batch request:
179
+
180
+ ```json
181
+ { "beanIds": ["bean-old", "bean-older"], "force": false }
182
+ ```
183
+
184
+ Batch response (summary):
185
+
186
+ ```json
187
+ {
188
+ "requestedCount": 2,
189
+ "deletedCount": 2,
190
+ "failedCount": 0,
191
+ "results": [
192
+ { "beanId": "bean-old", "deleted": true },
193
+ { "beanId": "bean-older", "deleted": true }
194
+ ]
195
+ }
196
+ ```
197
+
147
198
  ### beans_reopen
148
199
 
149
200
  Request:
@@ -199,6 +250,12 @@ Sort (modes: `status-priority-type-title`, `updated`, `created`, `id`):
199
250
  { "operation": "sort", "mode": "updated" }
200
251
  ```
201
252
 
253
+ Ready (actionable beans only):
254
+
255
+ ```json
256
+ { "operation": "ready" }
257
+ ```
258
+
202
259
  LLM context (generate Copilot instructions; optional write-to-workspace):
203
260
 
204
261
  ```json
@@ -211,7 +268,7 @@ Response (structuredContent):
211
268
  {
212
269
  "graphqlSchema": "...",
213
270
  "generatedInstructions": "...",
214
- "instructionsPath": "/workspace/.github/instructions/tasks.instructions.md"
271
+ "instructionsPath": "/workspace/.github/instructions/beans-prime.instructions.md"
215
272
  }
216
273
  ```
217
274
 
@@ -22747,7 +22747,7 @@ var init_utils = __esm({
22747
22747
  });
22748
22748
 
22749
22749
  // src/internal/graphql.ts
22750
- var LIST_BEANS_QUERY, CREATE_BEAN_MUTATION, UPDATE_BEAN_MUTATION, DELETE_BEAN_MUTATION;
22750
+ var LIST_BEANS_QUERY, CREATE_BEAN_MUTATION, UPDATE_BEAN_MUTATION, UPDATE_BEAN_MUTATION_WITH_IF_MATCH, DELETE_BEAN_MUTATION;
22751
22751
  var init_graphql = __esm({
22752
22752
  "src/internal/graphql.ts"() {
22753
22753
  "use strict";
@@ -22765,6 +22765,11 @@ var init_graphql = __esm({
22765
22765
  mutation($id: ID!, $input: UpdateBeanInput!) {
22766
22766
  updateBean(id: $id, input: $input) { id slug path title body status type priority tags parentId blockingIds blockedByIds createdAt updatedAt etag }
22767
22767
  }
22768
+ `;
22769
+ UPDATE_BEAN_MUTATION_WITH_IF_MATCH = `
22770
+ mutation($id: ID!, $input: UpdateBeanInput!, $ifMatch: String!) {
22771
+ updateBean(id: $id, input: $input, ifMatch: $ifMatch) { id slug path title body status type priority tags parentId blockingIds blockedByIds createdAt updatedAt etag }
22772
+ }
22768
22773
  `;
22769
22774
  DELETE_BEAN_MUTATION = `
22770
22775
  mutation($id: ID!) {
@@ -22922,10 +22927,50 @@ Output: ${stdout.slice(0, 1e3)}`
22922
22927
  if (updates.body !== void 0) {
22923
22928
  updateInput.body = updates.body;
22924
22929
  }
22925
- const { data, errors } = await this.executeGraphQL(UPDATE_BEAN_MUTATION, {
22926
- id: beanId,
22927
- input: updateInput
22928
- });
22930
+ const bodyMod = {};
22931
+ if (updates.bodyAppend !== void 0) {
22932
+ bodyMod.append = updates.bodyAppend;
22933
+ }
22934
+ if (Array.isArray(updates.bodyReplace) && updates.bodyReplace.length > 0) {
22935
+ bodyMod.replace = updates.bodyReplace;
22936
+ }
22937
+ if (Object.keys(bodyMod).length > 0) {
22938
+ updateInput.bodyMod = bodyMod;
22939
+ }
22940
+ let data;
22941
+ let errors;
22942
+ if (updates.ifMatch) {
22943
+ try {
22944
+ const res = await this.executeGraphQL(UPDATE_BEAN_MUTATION_WITH_IF_MATCH, {
22945
+ id: beanId,
22946
+ input: updateInput,
22947
+ ifMatch: updates.ifMatch
22948
+ });
22949
+ data = res.data;
22950
+ errors = res.errors;
22951
+ } catch (error48) {
22952
+ const message = error48.message || "";
22953
+ const unsupportedIfMatch = /unknown argument.*ifMatch|unknown field.*ifMatch|ifMatch.*not defined|field .*updateBean.* argument .*ifMatch/i.test(
22954
+ message
22955
+ );
22956
+ if (!unsupportedIfMatch) {
22957
+ throw error48;
22958
+ }
22959
+ const fallback = await this.executeGraphQL(UPDATE_BEAN_MUTATION, {
22960
+ id: beanId,
22961
+ input: updateInput
22962
+ });
22963
+ data = fallback.data;
22964
+ errors = fallback.errors;
22965
+ }
22966
+ } else {
22967
+ const res = await this.executeGraphQL(UPDATE_BEAN_MUTATION, {
22968
+ id: beanId,
22969
+ input: updateInput
22970
+ });
22971
+ data = res.data;
22972
+ errors = res.errors;
22973
+ }
22929
22974
  if (errors && errors.length > 0) {
22930
22975
  throw new Error(`GraphQL error: ${errors.map((e) => e.message).join(", ")}`);
22931
22976
  }
@@ -22945,6 +22990,21 @@ Output: ${stdout.slice(0, 1e3)}`
22945
22990
  const content = await (0, import_promises.readFile)(configPath, "utf8");
22946
22991
  return { configPath, content };
22947
22992
  }
22993
+ async primeInstructions() {
22994
+ const { stdout } = await execFileAsync(this.cliPath, ["prime"], {
22995
+ cwd: this.workspaceRoot,
22996
+ env: this.getSafeEnv(),
22997
+ maxBuffer: 10 * 1024 * 1024,
22998
+ timeout: 3e4
22999
+ });
23000
+ return stdout.trim();
23001
+ }
23002
+ async writeInstructions(instructions) {
23003
+ const instructionsPath = (0, import_node_path2.join)(this.workspaceRoot, ".github", "instructions", "beans-prime.instructions.md");
23004
+ await (0, import_promises.mkdir)((0, import_node_path2.dirname)(instructionsPath), { recursive: true });
23005
+ await (0, import_promises.writeFile)(instructionsPath, instructions, "utf8");
23006
+ return instructionsPath;
23007
+ }
22948
23008
  async graphqlSchema() {
22949
23009
  const { stdout } = await execFileAsync(this.cliPath, ["graphql", "--schema"], {
22950
23010
  cwd: this.workspaceRoot,
@@ -31072,6 +31132,10 @@ var EMPTY_COMPLETION_RESULT = {
31072
31132
  }
31073
31133
  };
31074
31134
 
31135
+ // src/server/BeansMcpServer.ts
31136
+ var import_node_child_process2 = require("child_process");
31137
+ var import_node_util2 = require("util");
31138
+
31075
31139
  // src/internal/queryHelpers.ts
31076
31140
  function sortBeansInternal(beans, mode) {
31077
31141
  const sorted = [...beans];
@@ -31127,15 +31191,23 @@ async function handleQueryOperation(backend, params) {
31127
31191
  const { operation, mode, statuses, types, search, tags, writeToWorkspaceInstructions, includeClosed } = params;
31128
31192
  if (operation === "llm_context") {
31129
31193
  const graphqlSchema = typeof backend.graphqlSchema === "function" ? await backend.graphqlSchema() : "";
31130
- const instructionsPath = writeToWorkspaceInstructions && typeof backend.writeInstructions === "function" ? await backend.writeInstructions("") : null;
31194
+ let generatedInstructions = "";
31195
+ if (typeof backend.primeInstructions === "function") {
31196
+ try {
31197
+ generatedInstructions = await backend.primeInstructions();
31198
+ } catch {
31199
+ generatedInstructions = "";
31200
+ }
31201
+ }
31202
+ const instructionsPath = writeToWorkspaceInstructions && typeof backend.writeInstructions === "function" ? await backend.writeInstructions(generatedInstructions) : null;
31131
31203
  return {
31132
31204
  content: [
31133
31205
  {
31134
31206
  type: "text",
31135
- text: JSON.stringify({ graphqlSchema, generatedInstructions: "", instructionsPath }, null, 2)
31207
+ text: JSON.stringify({ graphqlSchema, generatedInstructions, instructionsPath }, null, 2)
31136
31208
  }
31137
31209
  ],
31138
- structuredContent: { graphqlSchema, generatedInstructions: "", instructionsPath }
31210
+ structuredContent: { graphqlSchema, generatedInstructions, instructionsPath }
31139
31211
  };
31140
31212
  }
31141
31213
  if (operation === "open_config") {
@@ -31181,6 +31253,31 @@ async function handleQueryOperation(backend, params) {
31181
31253
  structuredContent: { query: search, count: beans2.length, beans: beans2 }
31182
31254
  };
31183
31255
  }
31256
+ if (operation === "ready") {
31257
+ const allBeans = await backend.list();
31258
+ const byId = new Map(allBeans.map((bean) => [bean.id, bean]));
31259
+ const candidates = await backend.list({ status: normalizedStatuses, type: normalizedTypes, search });
31260
+ const readyBeans = candidates.filter((bean) => {
31261
+ if (bean.status !== "todo") {
31262
+ return false;
31263
+ }
31264
+ const blockedBy = bean.blockedByIds || [];
31265
+ if (blockedBy.length === 0) {
31266
+ return true;
31267
+ }
31268
+ return blockedBy.every((blockerId) => {
31269
+ const blocker = byId.get(blockerId);
31270
+ if (!blocker) {
31271
+ return false;
31272
+ }
31273
+ return blocker.status === "completed" || blocker.status === "scrapped";
31274
+ });
31275
+ });
31276
+ return {
31277
+ content: [{ type: "text", text: JSON.stringify({ count: readyBeans.length, beans: readyBeans }, null, 2) }],
31278
+ structuredContent: { count: readyBeans.length, beans: readyBeans }
31279
+ };
31280
+ }
31184
31281
  const beans = await backend.list({ status: normalizedStatuses, type: normalizedTypes, search });
31185
31282
  const sorted = sortBeansInternal(beans, mode ?? "status-priority-type-title");
31186
31283
  return {
@@ -31203,7 +31300,7 @@ init_utils();
31203
31300
  // package.json
31204
31301
  var package_default = {
31205
31302
  name: "@selfagency/beans-mcp",
31206
- version: "0.1.3",
31303
+ version: "0.4.2",
31207
31304
  private: false,
31208
31305
  description: "MCP (Model Context Protocol) server for Beans issue tracker",
31209
31306
  author: {
@@ -31219,6 +31316,7 @@ var package_default = {
31219
31316
  type: "git",
31220
31317
  url: "git+https://github.com/selfagency/beans-mcp.git"
31221
31318
  },
31319
+ mcpName: "io.github.selfagency/beans-mcp",
31222
31320
  keywords: [
31223
31321
  "beans",
31224
31322
  "mcp",
@@ -31257,18 +31355,18 @@ var package_default = {
31257
31355
  devDependencies: {
31258
31356
  "@modelcontextprotocol/sdk": "^1.27.1",
31259
31357
  "@octokit/rest": "^22.0.1",
31260
- "@types/node": "^20.19.0",
31261
- "@vitest/coverage-v8": "^4.0.18",
31262
- "@vitest/ui": "4.0.18",
31358
+ "@types/node": "25.5.0",
31359
+ "@vitest/coverage-v8": "^4.1.0",
31360
+ "@vitest/ui": "4.1.0",
31263
31361
  husky: "^9.1.7",
31264
- "lint-staged": "^16.2.7",
31362
+ "lint-staged": "^16.3.3",
31265
31363
  ora: "^9.3.0",
31266
- oxfmt: "^0.35.0",
31267
- oxlint: "^1.50.0",
31268
- "oxlint-tsgolint": "^0.15.0",
31364
+ oxfmt: "^0.40.0",
31365
+ oxlint: "^1.55.0",
31366
+ "oxlint-tsgolint": "^0.16.0",
31269
31367
  tsup: "8.5.1",
31270
31368
  typescript: "^5.9.3",
31271
- vitest: "4.0.18",
31369
+ vitest: "4.1.0",
31272
31370
  zod: "4.3.6",
31273
31371
  zx: "^8.8.5"
31274
31372
  },
@@ -31284,6 +31382,45 @@ var package_default = {
31284
31382
  };
31285
31383
 
31286
31384
  // src/server/BeansMcpServer.ts
31385
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
31386
+ var PACKAGE_VERSION = package_default.version ?? "0.0.0-dev";
31387
+ function getSafeCliEnv(env) {
31388
+ const whitelist = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "SHELL"];
31389
+ const safeEnv = {};
31390
+ for (const key of whitelist) {
31391
+ if (env[key]) {
31392
+ safeEnv[key] = env[key];
31393
+ }
31394
+ }
31395
+ for (const key in env) {
31396
+ if (key.startsWith("BEANS_")) {
31397
+ safeEnv[key] = env[key];
31398
+ }
31399
+ }
31400
+ return safeEnv;
31401
+ }
31402
+ function extractVersionFromOutput(output) {
31403
+ const trimmed = output.trim();
31404
+ if (!trimmed) {
31405
+ return null;
31406
+ }
31407
+ const match = trimmed.match(/(?:^|[^\d])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/);
31408
+ return match?.[1] ?? null;
31409
+ }
31410
+ async function detectBeansCliVersion(cliPath, workspaceRoot) {
31411
+ try {
31412
+ const { stdout, stderr } = await execFileAsync2(cliPath, ["version"], {
31413
+ cwd: workspaceRoot,
31414
+ env: getSafeCliEnv(process.env),
31415
+ maxBuffer: 1024 * 1024,
31416
+ timeout: 5e3
31417
+ });
31418
+ return extractVersionFromOutput(`${stdout}
31419
+ ${stderr}`);
31420
+ } catch {
31421
+ return null;
31422
+ }
31423
+ }
31287
31424
  async function getBeanById(backend, beanId) {
31288
31425
  try {
31289
31426
  const beans = await backend.list();
@@ -31303,7 +31440,35 @@ function initHandler(backend) {
31303
31440
  };
31304
31441
  }
31305
31442
  function viewHandler(backend) {
31306
- return async ({ beanId }) => makeTextAndStructured({ bean: await getBeanById(backend, beanId) });
31443
+ return async ({ beanId, beanIds }) => {
31444
+ const ids = Array.isArray(beanIds) && beanIds.length > 0 ? beanIds : beanId ? [beanId] : [];
31445
+ if (ids.length === 0) {
31446
+ throw new Error("Either beanId or beanIds must be provided");
31447
+ }
31448
+ if (ids.length === 1) {
31449
+ const bean = await getBeanById(backend, ids[0]);
31450
+ return makeTextAndStructured({ bean });
31451
+ }
31452
+ const beans = await backend.list();
31453
+ const byId = new Map(beans.map((b) => [b.id, b]));
31454
+ const found = ids.map((id) => byId.get(id)).filter(Boolean);
31455
+ const missingBeanIds = ids.filter((id) => !byId.has(id));
31456
+ return makeTextAndStructured({ beans: found, missingBeanIds, count: found.length, requestedCount: ids.length });
31457
+ };
31458
+ }
31459
+ async function checkVersionCompatibility(cliPath, workspaceRoot, detector) {
31460
+ const detectedBeansVersion = await detector(cliPath, workspaceRoot);
31461
+ if (!detectedBeansVersion) {
31462
+ console.error(
31463
+ `[beans-mcp] warning: unable to determine Beans CLI version from \`${cliPath}\`; proceeding without version compatibility checks.`
31464
+ );
31465
+ return;
31466
+ }
31467
+ if (detectedBeansVersion !== PACKAGE_VERSION) {
31468
+ console.error(
31469
+ `[beans-mcp] warning: version mismatch detected (beans=${detectedBeansVersion}, beans-mcp=${PACKAGE_VERSION}); continuing startup.`
31470
+ );
31471
+ }
31307
31472
  }
31308
31473
  function createHandler(backend) {
31309
31474
  return async (input) => makeTextAndStructured({ bean: await backend.create(input) });
@@ -31339,17 +31504,56 @@ function updateHandler(backend) {
31339
31504
  clearParent: input.clearParent,
31340
31505
  blocking: input.blocking,
31341
31506
  blockedBy: input.blockedBy,
31342
- body: input.body
31507
+ body: input.body,
31508
+ bodyAppend: input.bodyAppend,
31509
+ bodyReplace: input.bodyReplace,
31510
+ ifMatch: input.ifMatch
31343
31511
  })
31344
31512
  });
31345
31513
  }
31346
31514
  function deleteHandler(backend) {
31347
- return async ({ beanId, force }) => {
31348
- const bean = await getBeanById(backend, beanId);
31349
- if (!force && bean.status !== "draft" && bean.status !== "scrapped") {
31350
- throw new Error("Only draft and scrapped beans are deletable unless force=true");
31515
+ return async ({ beanId, beanIds, force }) => {
31516
+ const ids = Array.isArray(beanIds) && beanIds.length > 0 ? beanIds : beanId ? [beanId] : [];
31517
+ if (ids.length === 0) {
31518
+ throw new Error("Either beanId or beanIds must be provided");
31519
+ }
31520
+ if (ids.length === 1) {
31521
+ const bean = await getBeanById(backend, ids[0]);
31522
+ if (!force && bean.status !== "draft" && bean.status !== "scrapped") {
31523
+ throw new Error("Only draft and scrapped beans are deletable unless force=true");
31524
+ }
31525
+ return makeTextAndStructured(await backend.delete(ids[0]));
31351
31526
  }
31352
- return makeTextAndStructured(await backend.delete(beanId));
31527
+ const beans = await backend.list();
31528
+ const byId = new Map(beans.map((b) => [b.id, b]));
31529
+ const results = [];
31530
+ for (const id of ids) {
31531
+ const bean = byId.get(id);
31532
+ if (!bean) {
31533
+ results.push({ beanId: id, deleted: false, error: "Bean not found" });
31534
+ continue;
31535
+ }
31536
+ if (!force && bean.status !== "draft" && bean.status !== "scrapped") {
31537
+ results.push({
31538
+ beanId: id,
31539
+ deleted: false,
31540
+ error: "Only draft and scrapped beans are deletable unless force=true"
31541
+ });
31542
+ continue;
31543
+ }
31544
+ try {
31545
+ await backend.delete(id);
31546
+ results.push({ beanId: id, deleted: true });
31547
+ } catch (error48) {
31548
+ results.push({ beanId: id, deleted: false, error: error48.message });
31549
+ }
31550
+ }
31551
+ return makeTextAndStructured({
31552
+ results,
31553
+ requestedCount: ids.length,
31554
+ deletedCount: results.filter((r) => r.deleted).length,
31555
+ failedCount: results.filter((r) => !r.deleted).length
31556
+ });
31353
31557
  };
31354
31558
  }
31355
31559
  function queryHandler(backend) {
@@ -31410,7 +31614,12 @@ function registerTools(server, backend) {
31410
31614
  {
31411
31615
  title: "View Bean",
31412
31616
  description: "Fetch full bean details by ID.",
31413
- inputSchema: external_exports3.object({ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH) }),
31617
+ inputSchema: external_exports3.object({
31618
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
31619
+ beanIds: external_exports3.array(external_exports3.string().min(1).max(MAX_ID_LENGTH)).optional()
31620
+ }).refine((input) => Boolean(input.beanId) || Array.isArray(input.beanIds) && input.beanIds.length > 0, {
31621
+ message: "Either beanId or beanIds must be provided"
31622
+ }),
31414
31623
  annotations: {
31415
31624
  readOnlyHint: true,
31416
31625
  destructiveHint: false,
@@ -31499,8 +31708,21 @@ function registerTools(server, backend) {
31499
31708
  clearParent: external_exports3.boolean().optional(),
31500
31709
  blocking: external_exports3.array(external_exports3.string().max(MAX_ID_LENGTH)).optional(),
31501
31710
  blockedBy: external_exports3.array(external_exports3.string().max(MAX_ID_LENGTH)).optional(),
31502
- body: external_exports3.string().max(MAX_DESCRIPTION_LENGTH).optional()
31503
- }),
31711
+ body: external_exports3.string().max(MAX_DESCRIPTION_LENGTH).optional(),
31712
+ bodyAppend: external_exports3.string().max(MAX_DESCRIPTION_LENGTH).optional(),
31713
+ bodyReplace: external_exports3.array(
31714
+ external_exports3.object({
31715
+ old: external_exports3.string().max(MAX_DESCRIPTION_LENGTH),
31716
+ new: external_exports3.string().max(MAX_DESCRIPTION_LENGTH)
31717
+ })
31718
+ ).optional(),
31719
+ ifMatch: external_exports3.string().max(MAX_METADATA_LENGTH).optional()
31720
+ }).refine(
31721
+ (input) => !(input.body !== void 0 && (input.bodyAppend !== void 0 || input.bodyReplace !== void 0)),
31722
+ {
31723
+ message: "body cannot be combined with bodyAppend/bodyReplace"
31724
+ }
31725
+ ),
31504
31726
  annotations: {
31505
31727
  readOnlyHint: false,
31506
31728
  destructiveHint: false,
@@ -31516,8 +31738,11 @@ function registerTools(server, backend) {
31516
31738
  title: "Delete Bean",
31517
31739
  description: "Delete a bean (intended for draft/scrapped beans).",
31518
31740
  inputSchema: external_exports3.object({
31519
- beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH),
31741
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
31742
+ beanIds: external_exports3.array(external_exports3.string().min(1).max(MAX_ID_LENGTH)).optional(),
31520
31743
  force: external_exports3.boolean().default(false)
31744
+ }).refine((input) => Boolean(input.beanId) || Array.isArray(input.beanIds) && input.beanIds.length > 0, {
31745
+ message: "Either beanId or beanIds must be provided"
31521
31746
  }),
31522
31747
  annotations: {
31523
31748
  readOnlyHint: false,
@@ -31534,7 +31759,7 @@ function registerTools(server, backend) {
31534
31759
  title: "Query Beans",
31535
31760
  description: "Unified query tool for refresh, filter, search, and sort operations.",
31536
31761
  inputSchema: external_exports3.object({
31537
- operation: external_exports3.enum(["refresh", "filter", "search", "sort", "llm_context", "open_config"]).default("refresh"),
31762
+ operation: external_exports3.enum(["refresh", "filter", "search", "sort", "ready", "llm_context", "open_config"]).default("refresh"),
31538
31763
  mode: external_exports3.enum(["status-priority-type-title", "updated", "created", "id"]).optional(),
31539
31764
  statuses: external_exports3.array(external_exports3.string().max(MAX_METADATA_LENGTH)).nullable().optional(),
31540
31765
  types: external_exports3.array(external_exports3.string().max(MAX_METADATA_LENGTH)).nullable().optional(),
@@ -31616,6 +31841,12 @@ var MutableBackend = class {
31616
31841
  openConfig() {
31617
31842
  return this.inner.openConfig();
31618
31843
  }
31844
+ primeInstructions() {
31845
+ return this.inner.primeInstructions?.() ?? Promise.resolve("");
31846
+ }
31847
+ writeInstructions(instructions) {
31848
+ return this.inner.writeInstructions?.(instructions) ?? Promise.resolve(null);
31849
+ }
31619
31850
  graphqlSchema() {
31620
31851
  return this.inner.graphqlSchema();
31621
31852
  }
@@ -31653,7 +31884,7 @@ async function createBeansMcpServer(opts) {
31653
31884
  const backend = opts.backend || new BeansCliBackend2(opts.workspaceRoot, opts.cliPath || "beans", opts.logDir);
31654
31885
  const server = new McpServer({
31655
31886
  name: opts.name || "beans-mcp-server",
31656
- version: opts.version || "0.1.0"
31887
+ version: opts.version || PACKAGE_VERSION
31657
31888
  });
31658
31889
  registerTools(server, backend);
31659
31890
  return { server, backend };
@@ -31725,17 +31956,17 @@ function parseCliArgs(argv) {
31725
31956
  }
31726
31957
  return { workspaceRoot, workspaceExplicit, cliPath, port, logDir };
31727
31958
  }
31728
- async function startBeansMcpServer(argv, _resolveRoots) {
31959
+ async function startBeansMcpServer(argv, _resolveRoots, _detectBeansVersion) {
31729
31960
  const { BeansCliBackend: BeansCliBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
31730
31961
  const { StdioServerTransport: StdioServerTransport2 } = await Promise.resolve().then(() => (init_stdio2(), stdio_exports));
31731
31962
  const { workspaceRoot, workspaceExplicit, cliPath, port, logDir } = parseCliArgs(argv);
31963
+ let effectiveWorkspaceRoot = workspaceRoot;
31732
31964
  process.env.BEANS_VSCODE_MCP_PORT = String(port);
31733
31965
  process.env.BEANS_MCP_PORT = String(port);
31734
31966
  try {
31735
- const version2 = package_default.version ?? "0.0.0-dev";
31736
31967
  const workspaceLabel = workspaceExplicit ? workspaceRoot : "(auto from roots)";
31737
31968
  console.error(
31738
- `[beans-mcp] v${version2} starting (port=${port}, workspace=${workspaceLabel}, cli=${cliPath}, logDir=${logDir})`
31969
+ `[beans-mcp] v${PACKAGE_VERSION} starting (port=${port}, workspace=${workspaceLabel}, cli=${cliPath}, logDir=${logDir})`
31739
31970
  );
31740
31971
  } catch {
31741
31972
  }
@@ -31752,13 +31983,17 @@ async function startBeansMcpServer(argv, _resolveRoots) {
31752
31983
  const resolver = _resolveRoots ?? resolveWorkspaceFromRoots;
31753
31984
  const rootPath = await resolver(server);
31754
31985
  if (rootPath) {
31755
- mutable.setInner(new BeansCliBackend2(rootPath, cliPath));
31986
+ mutable.setInner(new BeansCliBackend2(rootPath, cliPath, logDir));
31987
+ effectiveWorkspaceRoot = rootPath;
31756
31988
  try {
31757
31989
  console.error(`[beans-mcp] workspace resolved from roots: ${rootPath}`);
31758
31990
  } catch {
31759
31991
  }
31760
31992
  }
31761
31993
  }
31994
+ const beansVersionDetector = _detectBeansVersion ?? detectBeansCliVersion;
31995
+ void checkVersionCompatibility(cliPath, effectiveWorkspaceRoot, beansVersionDetector).catch(() => {
31996
+ });
31762
31997
  }
31763
31998
 
31764
31999
  // src/cli.ts