@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.
- package/README.md +63 -6
- package/{dist/beans-mcp-server.cjs → beans-mcp-server.cjs} +269 -34
- package/{dist/index.cjs → index.cjs} +269 -34
- package/{dist/index.d.ts → index.d.ts} +19 -1
- package/{dist/index.js → index.js} +269 -34
- package/package.json +28 -64
- package/.beans.yml +0 -6
- package/.claude/settings.local.json +0 -18
- package/.editorconfig +0 -13
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/release.yml +0 -235
- package/.github/workflows/test.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.oxfmtrc.json +0 -11
- package/.oxlintrc.json +0 -37
- package/.vscode/settings.json +0 -3
- package/CHANGELOG.md +0 -160
- package/CONTRIBUTING.md +0 -139
- package/LICENSE.txt +0 -21
- package/codeql/codeql-custom-queries-actions/README.md +0 -14
- package/codeql/codeql-custom-queries-actions/codeql-pack.lock.yml +0 -32
- package/codeql/codeql-custom-queries-actions/codeql-pack.yml +0 -7
- package/codeql/codeql-custom-queries-actions/qlpack.yml +0 -6
- package/codeql/codeql-custom-queries-actions/queries/github-script-without-tojson.ql +0 -18
- package/codeql/codeql-custom-queries-actions/queries/strict-external-action-pinning.ql +0 -18
- package/codeql/codeql-custom-queries-javascript/README.md +0 -14
- package/codeql/codeql-custom-queries-javascript/codeql-pack.lock.yml +0 -30
- package/codeql/codeql-custom-queries-javascript/codeql-pack.yml +0 -7
- package/codeql/codeql-custom-queries-javascript/qlpack.yml +0 -6
- package/codeql/codeql-custom-queries-javascript/queries/child-process-shell-apis.ql +0 -26
- package/codeql/codeql-custom-queries-javascript/queries/innerhtml-assignment.ql +0 -24
- package/dist/README.md +0 -307
- package/dist/beans-mcp-server.cjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/package.json +0 -43
- package/pnpm-workspace.yaml +0 -2
- package/scripts/release.js +0 -433
- package/scripts/write-dist-package.js +0 -53
- package/src/cli.ts +0 -14
- package/src/index.ts +0 -21
- package/src/internal/graphql.ts +0 -33
- package/src/internal/queryHelpers.ts +0 -157
- package/src/server/BeansMcpServer.ts +0 -623
- package/src/server/backend.ts +0 -364
- package/src/test/BeansMcpServer.test.ts +0 -514
- package/src/test/handlers.unit.test.ts +0 -201
- package/src/test/parseCliArgs.test.ts +0 -69
- package/src/test/protocol.e2e.test.ts +0 -884
- package/src/test/queryHelpers.test.ts +0 -524
- package/src/test/startBeansMcpServer.test.ts +0 -146
- package/src/test/tools-integration.test.ts +0 -912
- package/src/test/utils.test.ts +0 -81
- package/src/types.ts +0 -46
- package/src/utils.ts +0 -20
- package/tsconfig.json +0 -24
- package/tsup.config.ts +0 -42
- 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
|
|
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/
|
|
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
|
|
22926
|
-
|
|
22927
|
-
|
|
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
|
-
|
|
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
|
|
31207
|
+
text: JSON.stringify({ graphqlSchema, generatedInstructions, instructionsPath }, null, 2)
|
|
31136
31208
|
}
|
|
31137
31209
|
],
|
|
31138
|
-
structuredContent: { graphqlSchema, generatedInstructions
|
|
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.
|
|
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": "
|
|
31261
|
-
"@vitest/coverage-v8": "^4.0
|
|
31262
|
-
"@vitest/ui": "4.0
|
|
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.
|
|
31362
|
+
"lint-staged": "^16.3.3",
|
|
31265
31363
|
ora: "^9.3.0",
|
|
31266
|
-
oxfmt: "^0.
|
|
31267
|
-
oxlint: "^1.
|
|
31268
|
-
"oxlint-tsgolint": "^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
|
|
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 }) =>
|
|
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
|
|
31349
|
-
if (
|
|
31350
|
-
throw new Error("
|
|
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
|
-
|
|
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({
|
|
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 ||
|
|
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${
|
|
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
|