@selfagency/beans-mcp 0.1.2 → 0.1.3

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 (57) hide show
  1. package/.beans.yml +6 -0
  2. package/.claude/settings.local.json +18 -0
  3. package/.editorconfig +13 -0
  4. package/.github/dependabot.yml +11 -0
  5. package/.github/workflows/release.yml +235 -0
  6. package/.github/workflows/test.yml +84 -0
  7. package/.husky/pre-commit +1 -0
  8. package/.nvmrc +1 -0
  9. package/.oxfmtrc.json +11 -0
  10. package/.oxlintrc.json +37 -0
  11. package/.vscode/settings.json +3 -0
  12. package/CHANGELOG.md +160 -0
  13. package/CONTRIBUTING.md +139 -0
  14. package/codeql/codeql-custom-queries-actions/README.md +14 -0
  15. package/codeql/codeql-custom-queries-actions/codeql-pack.lock.yml +32 -0
  16. package/codeql/codeql-custom-queries-actions/codeql-pack.yml +7 -0
  17. package/codeql/codeql-custom-queries-actions/qlpack.yml +6 -0
  18. package/codeql/codeql-custom-queries-actions/queries/github-script-without-tojson.ql +18 -0
  19. package/codeql/codeql-custom-queries-actions/queries/strict-external-action-pinning.ql +18 -0
  20. package/codeql/codeql-custom-queries-javascript/README.md +14 -0
  21. package/codeql/codeql-custom-queries-javascript/codeql-pack.lock.yml +30 -0
  22. package/codeql/codeql-custom-queries-javascript/codeql-pack.yml +7 -0
  23. package/codeql/codeql-custom-queries-javascript/qlpack.yml +6 -0
  24. package/codeql/codeql-custom-queries-javascript/queries/child-process-shell-apis.ql +26 -0
  25. package/codeql/codeql-custom-queries-javascript/queries/innerhtml-assignment.ql +24 -0
  26. package/dist/README.md +307 -0
  27. package/{beans-mcp-server.cjs → dist/beans-mcp-server.cjs} +97 -0
  28. package/dist/beans-mcp-server.cjs.map +1 -0
  29. package/{index.cjs → dist/index.cjs} +97 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/{index.js → dist/index.js} +97 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/package.json +43 -0
  34. package/package.json +63 -26
  35. package/pnpm-workspace.yaml +2 -0
  36. package/scripts/release.js +433 -0
  37. package/scripts/write-dist-package.js +53 -0
  38. package/src/cli.ts +14 -0
  39. package/src/index.ts +21 -0
  40. package/src/internal/graphql.ts +33 -0
  41. package/src/internal/queryHelpers.ts +157 -0
  42. package/src/server/BeansMcpServer.ts +623 -0
  43. package/src/server/backend.ts +364 -0
  44. package/src/test/BeansMcpServer.test.ts +514 -0
  45. package/src/test/handlers.unit.test.ts +201 -0
  46. package/src/test/parseCliArgs.test.ts +69 -0
  47. package/src/test/protocol.e2e.test.ts +884 -0
  48. package/src/test/queryHelpers.test.ts +524 -0
  49. package/src/test/startBeansMcpServer.test.ts +146 -0
  50. package/src/test/tools-integration.test.ts +912 -0
  51. package/src/test/utils.test.ts +81 -0
  52. package/src/types.ts +46 -0
  53. package/src/utils.ts +20 -0
  54. package/tsconfig.json +24 -0
  55. package/tsup.config.ts +42 -0
  56. package/vitest.config.ts +18 -0
  57. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isPathWithinRoot, makeTextAndStructured } from '../utils';
3
+
4
+ describe('isPathWithinRoot', () => {
5
+ it('should return true for paths within root', () => {
6
+ const root = '/workspace/.beans';
7
+ const target = '/workspace/.beans/file.md';
8
+ expect(isPathWithinRoot(root, target)).toBe(true);
9
+ });
10
+
11
+ it('should return false for paths outside root', () => {
12
+ const root = '/workspace/.beans';
13
+ const target = '/workspace/outside.md';
14
+ expect(isPathWithinRoot(root, target)).toBe(false);
15
+ });
16
+
17
+ it('should return false for paths with parent traversal', () => {
18
+ const root = '/workspace/.beans';
19
+ const target = '/workspace/.beans/../../etc/passwd';
20
+ expect(isPathWithinRoot(root, target)).toBe(false);
21
+ });
22
+
23
+ it('should handle relative path normalization', () => {
24
+ const root = '/workspace/.beans/';
25
+ const target = '/workspace/.beans/subdir/file.md';
26
+ expect(isPathWithinRoot(root, target)).toBe(true);
27
+ });
28
+
29
+ it('should return false when root and target are the same', () => {
30
+ const root = '/workspace/.beans';
31
+ const target = '/workspace/.beans';
32
+ expect(isPathWithinRoot(root, target)).toBe(false);
33
+ });
34
+
35
+ it('should handle paths with trailing slashes', () => {
36
+ const root = '/workspace/.beans/';
37
+ const target = '/workspace/.beans/file.md';
38
+ expect(isPathWithinRoot(root, target)).toBe(true);
39
+ });
40
+ });
41
+
42
+ describe('makeTextAndStructured', () => {
43
+ it('should serialize object as JSON text', () => {
44
+ const input = { name: 'test', value: 42 } as const;
45
+ const result = makeTextAndStructured(input);
46
+
47
+ expect(result.content).toHaveLength(1);
48
+ expect(result.content[0].type).toBe('text');
49
+ expect(() => JSON.parse(result.content[0].text)).not.toThrow();
50
+ expect(JSON.parse(result.content[0].text)).toEqual(input);
51
+ });
52
+
53
+ it('should preserve arbitrary fields', () => {
54
+ const input = { message: 'Operation completed', extra: { ok: true } } as const;
55
+ const result = makeTextAndStructured(input as any);
56
+ expect(JSON.parse(result.content[0].text)).toEqual(input);
57
+ });
58
+
59
+ it('should include nested bean objects in JSON text', () => {
60
+ const input = { bean: { id: 'b1', title: 'My Bean' } } as const;
61
+ const result = makeTextAndStructured(input as any);
62
+ const parsed = JSON.parse(result.content[0].text);
63
+ expect(parsed).toEqual(input);
64
+ });
65
+
66
+ it('should handle arrays in JSON text', () => {
67
+ const input = { items: [1, 2, 3] };
68
+ const result = makeTextAndStructured(input);
69
+ const parsed = JSON.parse(result.content[0].text);
70
+ expect(parsed.items).toEqual([1, 2, 3]);
71
+ });
72
+
73
+ it('should handle null and undefined values', () => {
74
+ const input = { nullValue: null, undefinedValue: undefined } as any;
75
+ const result = makeTextAndStructured(input);
76
+ const parsed = JSON.parse(result.content[0].text);
77
+ expect(parsed.nullValue).toBeNull();
78
+ // Note: undefined properties are omitted by JSON.stringify
79
+ expect(Object.prototype.hasOwnProperty.call(parsed, 'undefinedValue')).toBe(false);
80
+ });
81
+ });
package/src/types.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Public types for beans-mcp-server
3
+ */
4
+
5
+ export type SortMode =
6
+ | "status-priority-type-title"
7
+ | "updated"
8
+ | "created"
9
+ | "id";
10
+
11
+ export type BeanRecord = {
12
+ id: string;
13
+ slug: string;
14
+ path: string;
15
+ title: string;
16
+ body: string;
17
+ status: string;
18
+ type: string;
19
+ description?: string;
20
+ priority?: string;
21
+ tags?: string[];
22
+ parentId?: string;
23
+ blockingIds?: string[];
24
+ blockedByIds?: string[];
25
+ createdAt?: string;
26
+ updatedAt?: string;
27
+ etag?: string;
28
+ };
29
+
30
+ /**
31
+ * GraphQL error shape as returned by the Beans CLI GraphQL endpoint.
32
+ */
33
+ export type GraphQLError = {
34
+ message: string;
35
+ locations?: Array<{ line: number; column: number }>;
36
+ path?: Array<string | number>;
37
+ extensions?: Record<string, unknown>;
38
+ };
39
+
40
+ export const DEFAULT_MCP_PORT = 39173;
41
+
42
+ export const MAX_ID_LENGTH = 128;
43
+ export const MAX_TITLE_LENGTH = 1024;
44
+ export const MAX_METADATA_LENGTH = 128;
45
+ export const MAX_DESCRIPTION_LENGTH = 65536; // 64KB
46
+ export const MAX_PATH_LENGTH = 1024;
package/src/utils.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { isAbsolute, relative, resolve } from 'node:path';
2
+
3
+ /**
4
+ * Check whether `target` is contained within `root` after resolving both paths.
5
+ * Guards against the Windows cross-drive bypass where `path.relative(root, target)`
6
+ * returns an absolute path (e.g. `D:\evil`) that does not start with `..`.
7
+ */
8
+ export function isPathWithinRoot(root: string, target: string): boolean {
9
+ const resolvedRoot = resolve(root);
10
+ const resolvedTarget = resolve(target);
11
+ const rel = relative(resolvedRoot, resolvedTarget);
12
+ return !!rel && !rel.startsWith('..') && !isAbsolute(rel);
13
+ }
14
+
15
+ export function makeTextAndStructured<T extends Record<string, unknown>>(value: T) {
16
+ // Return JSON in text content only to avoid duplicate rendering in some clients.
17
+ return {
18
+ content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
19
+ };
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noImplicitReturns": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "allowSyntheticDefaultImports": true
21
+ },
22
+ "include": ["src/**/*.ts"],
23
+ "exclude": ["src/**/*.test.ts", "dist", "node_modules"]
24
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig([
4
+ {
5
+ // ESM library entry point
6
+ entry: { index: 'src/index.ts' },
7
+ format: ['esm'],
8
+ outDir: 'dist',
9
+ target: 'node18',
10
+ splitting: false,
11
+ dts: true,
12
+ sourcemap: process.env.NODE_ENV !== 'production',
13
+ clean: true,
14
+ },
15
+ {
16
+ // CJS library entry point
17
+ entry: { index: 'src/index.ts' },
18
+ format: ['cjs'],
19
+ outDir: 'dist',
20
+ outExtension: () => ({ js: '.cjs' }),
21
+ target: 'node18',
22
+ splitting: false,
23
+ cjsInterop: true,
24
+ dts: false,
25
+ sourcemap: process.env.NODE_ENV !== 'production',
26
+ },
27
+ {
28
+ // CLI binary entry point
29
+ entry: { 'beans-mcp-server': 'src/cli.ts' },
30
+ format: ['cjs'],
31
+ outDir: 'dist',
32
+ outExtension: () => ({ js: '.cjs' }),
33
+ target: 'node18',
34
+ splitting: false,
35
+ cjsInterop: true,
36
+ dts: false,
37
+ sourcemap: process.env.NODE_ENV !== 'production',
38
+ banner: {
39
+ js: '#!/usr/bin/env node',
40
+ },
41
+ },
42
+ ]);
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: false,
6
+ environment: "node",
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: ["text", "json", "html"],
10
+ exclude: [
11
+ "node_modules/",
12
+ "dist/",
13
+ "src/**/*.test.ts",
14
+ "src/server/backend.ts",
15
+ ],
16
+ },
17
+ },
18
+ });
File without changes