@selfagency/beans-mcp 0.1.0 → 0.1.1

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 (47) hide show
  1. package/.beans.yml +6 -0
  2. package/.claude/settings.local.json +18 -0
  3. package/.editorconfig +13 -0
  4. package/.github/workflows/release.yml +235 -0
  5. package/.github/workflows/test.yml +80 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.oxfmtrc.json +11 -0
  9. package/.oxlintrc.json +37 -0
  10. package/.vscode/settings.json +3 -0
  11. package/CHANGELOG.md +140 -0
  12. package/CONTRIBUTING.md +139 -0
  13. package/LICENSE.txt +21 -0
  14. package/README.md +4 -4
  15. package/dist/README.md +307 -0
  16. package/dist/beans-mcp-server.cjs.map +1 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.js +31676 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/package.json +43 -0
  21. package/package.json +64 -27
  22. package/pnpm-workspace.yaml +2 -0
  23. package/scripts/release.js +433 -0
  24. package/scripts/write-dist-package.js +53 -0
  25. package/src/cli.ts +14 -0
  26. package/src/index.ts +21 -0
  27. package/src/internal/graphql.ts +33 -0
  28. package/src/internal/queryHelpers.ts +157 -0
  29. package/src/server/BeansMcpServer.ts +600 -0
  30. package/src/server/backend.ts +358 -0
  31. package/src/test/BeansMcpServer.test.ts +514 -0
  32. package/src/test/handlers.unit.test.ts +184 -0
  33. package/src/test/parseCliArgs.test.ts +69 -0
  34. package/src/test/protocol.e2e.test.ts +884 -0
  35. package/src/test/queryHelpers.test.ts +524 -0
  36. package/src/test/startBeansMcpServer.test.ts +146 -0
  37. package/src/test/tools-integration.test.ts +912 -0
  38. package/src/test/utils.test.ts +80 -0
  39. package/src/types.ts +46 -0
  40. package/src/utils.ts +20 -0
  41. package/tsconfig.json +24 -0
  42. package/tsup.config.ts +42 -0
  43. package/vitest.config.ts +18 -0
  44. package/index.js +0 -15350
  45. /package/{beans-mcp-server.cjs → dist/beans-mcp-server.cjs} +0 -0
  46. /package/{index.cjs → dist/index.cjs} +0 -0
  47. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * GraphQL queries and mutations for Beans CLI
3
+ */
4
+
5
+ export const LIST_BEANS_QUERY = `
6
+ query($filter: BeanFilter) {
7
+ beans(filter: $filter) { id slug path title body status type priority tags parentId blockingIds blockedByIds createdAt updatedAt etag }
8
+ }
9
+ `;
10
+
11
+ export const SHOW_BEAN_QUERY = `
12
+ query($id: ID!) {
13
+ bean(id: $id) { id slug path title body status type priority tags parentId blockingIds blockedByIds createdAt updatedAt etag }
14
+ }
15
+ `;
16
+
17
+ export const CREATE_BEAN_MUTATION = `
18
+ mutation($input: CreateBeanInput!) {
19
+ createBean(input: $input) { id slug path title body status type priority tags parentId blockingIds blockedByIds createdAt updatedAt etag }
20
+ }
21
+ `;
22
+
23
+ export const UPDATE_BEAN_MUTATION = `
24
+ mutation($id: ID!, $input: UpdateBeanInput!) {
25
+ updateBean(id: $id, input: $input) { id slug path title body status type priority tags parentId blockingIds blockedByIds createdAt updatedAt etag }
26
+ }
27
+ `;
28
+
29
+ export const DELETE_BEAN_MUTATION = `
30
+ mutation($id: ID!) {
31
+ deleteBean(id: $id)
32
+ }
33
+ `;
@@ -0,0 +1,157 @@
1
+ import { BeanRecord, SortMode } from '../types';
2
+
3
+ export type QueryBackend = {
4
+ graphqlSchema?: () => Promise<string>;
5
+ writeInstructions?: (instructions: string) => Promise<string | null>;
6
+ openConfig?: () => Promise<Record<string, unknown>>;
7
+ list(options?: { status?: string[]; type?: string[]; search?: string }): Promise<BeanRecord[]>;
8
+ };
9
+
10
+ function sortBeansInternal(beans: BeanRecord[], mode: SortMode): BeanRecord[] {
11
+ const sorted = [...beans];
12
+ const statusWeight: Record<string, number> = {
13
+ 'in-progress': 0,
14
+ todo: 1,
15
+ draft: 2,
16
+ completed: 3,
17
+ scrapped: 4,
18
+ };
19
+ const priorityWeight: Record<string, number> = {
20
+ critical: 0,
21
+ high: 1,
22
+ normal: 2,
23
+ low: 3,
24
+ deferred: 4,
25
+ };
26
+ const typeWeight: Record<string, number> = {
27
+ milestone: 0,
28
+ epic: 1,
29
+ feature: 2,
30
+ bug: 3,
31
+ task: 4,
32
+ };
33
+
34
+ if (mode === 'updated') {
35
+ return sorted.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
36
+ }
37
+
38
+ if (mode === 'created') {
39
+ return sorted.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
40
+ }
41
+
42
+ if (mode === 'id') {
43
+ return sorted.sort((a, b) => a.id.localeCompare(b.id));
44
+ }
45
+
46
+ return sorted.sort((a, b) => {
47
+ const statusCmp = (statusWeight[a.status] ?? 99) - (statusWeight[b.status] ?? 99);
48
+ if (statusCmp !== 0) {
49
+ return statusCmp;
50
+ }
51
+
52
+ const aPriority = a.priority || 'normal';
53
+ const bPriority = b.priority || 'normal';
54
+ const priorityCmp = (priorityWeight[aPriority] ?? 99) - (priorityWeight[bPriority] ?? 99);
55
+ if (priorityCmp !== 0) {
56
+ return priorityCmp;
57
+ }
58
+
59
+ const typeCmp = (typeWeight[a.type] ?? 99) - (typeWeight[b.type] ?? 99);
60
+ if (typeCmp !== 0) {
61
+ return typeCmp;
62
+ }
63
+
64
+ return a.title.localeCompare(b.title);
65
+ });
66
+ }
67
+
68
+ export async function handleQueryOperation(
69
+ backend: QueryBackend,
70
+ params: {
71
+ operation: string;
72
+ mode?: SortMode;
73
+ statuses?: string[] | null;
74
+ types?: string[] | null;
75
+ search?: string;
76
+ tags?: string[] | null;
77
+ writeToWorkspaceInstructions?: boolean;
78
+ includeClosed?: boolean;
79
+ },
80
+ ): Promise<{ content: Array<{ type: 'text'; text: string }>; structuredContent: Record<string, unknown> }> {
81
+ const { operation, mode, statuses, types, search, tags, writeToWorkspaceInstructions, includeClosed } = params;
82
+
83
+ if (operation === 'llm_context') {
84
+ const graphqlSchema = typeof backend.graphqlSchema === 'function' ? await backend.graphqlSchema() : '';
85
+ const instructionsPath =
86
+ writeToWorkspaceInstructions && typeof backend.writeInstructions === 'function'
87
+ ? await backend.writeInstructions('')
88
+ : null;
89
+ return {
90
+ content: [
91
+ {
92
+ type: 'text',
93
+ text: JSON.stringify({ graphqlSchema, generatedInstructions: '', instructionsPath }, null, 2),
94
+ },
95
+ ],
96
+ structuredContent: { graphqlSchema, generatedInstructions: '', instructionsPath },
97
+ };
98
+ }
99
+
100
+ if (operation === 'open_config') {
101
+ const config = typeof backend.openConfig === 'function' ? await backend.openConfig() : {};
102
+ return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }], structuredContent: config };
103
+ }
104
+
105
+ const normalizedStatuses = Array.isArray(statuses) ? statuses : undefined;
106
+ const normalizedTypes = Array.isArray(types) ? types : undefined;
107
+
108
+ if (operation === 'refresh') {
109
+ const beans = await backend.list();
110
+ return {
111
+ content: [{ type: 'text', text: JSON.stringify({ count: beans.length, beans }, null, 2) }],
112
+ structuredContent: { count: beans.length, beans },
113
+ };
114
+ }
115
+
116
+ if (operation === 'filter') {
117
+ let beans = await backend.list({ status: normalizedStatuses, type: normalizedTypes, search });
118
+ if (Array.isArray(tags) && tags.length > 0) {
119
+ const tagSet = new Set(tags);
120
+ beans = beans.filter((bean: BeanRecord) => (bean.tags || []).some((tag: string) => tagSet.has(tag)));
121
+ }
122
+ return {
123
+ content: [{ type: 'text', text: JSON.stringify({ count: beans.length, beans }, null, 2) }],
124
+ structuredContent: { count: beans.length, beans },
125
+ };
126
+ }
127
+
128
+ if (operation === 'search') {
129
+ let beans = await backend.list({ search });
130
+ if (typeof search === 'string' && search.length > 0) {
131
+ const q = search.toLowerCase();
132
+ beans = beans.filter((b: BeanRecord) => {
133
+ const title = (b.title || '').toLowerCase();
134
+ const id = (b.id || '').toLowerCase();
135
+ const tagsStr = (b.tags || []).join(' ').toLowerCase();
136
+ return title.includes(q) || id.includes(q) || tagsStr.includes(q);
137
+ });
138
+ }
139
+ if (includeClosed === false) {
140
+ beans = beans.filter((b: BeanRecord) => b.status !== 'completed' && b.status !== 'scrapped');
141
+ }
142
+ return {
143
+ content: [{ type: 'text', text: JSON.stringify({ query: search, count: beans.length, beans }, null, 2) }],
144
+ structuredContent: { query: search, count: beans.length, beans },
145
+ };
146
+ }
147
+
148
+ // sort
149
+ const beans = await backend.list({ status: normalizedStatuses, type: normalizedTypes, search });
150
+ const sorted = sortBeansInternal(beans, mode ?? 'status-priority-type-title');
151
+ return {
152
+ content: [{ type: 'text', text: JSON.stringify({ mode, count: beans.length, beans: sorted }, null, 2) }],
153
+ structuredContent: { mode, count: beans.length, beans: sorted },
154
+ };
155
+ }
156
+
157
+ export { sortBeansInternal as sortBeans };