ocean-brain 0.1.6 → 0.2.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.
package/dist/index.js CHANGED
@@ -5,10 +5,21 @@ import { execSync } from "child_process";
5
5
  import fs from "fs";
6
6
  import os from "os";
7
7
  import path from "path";
8
- import { fileURLToPath } from "url";
8
+ import { fileURLToPath, pathToFileURL } from "url";
9
9
  import { Command } from "commander";
10
10
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  var serverRoot = path.resolve(__dirname, "..", "server");
12
+ function findBin(name, fromDir) {
13
+ let dir = path.resolve(fromDir, "..");
14
+ while (true) {
15
+ const bin = path.resolve(dir, "node_modules", ".bin", name);
16
+ if (fs.existsSync(bin)) return bin;
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir) break;
19
+ dir = parent;
20
+ }
21
+ throw new Error(`Could not find "${name}" binary. Make sure it is installed.`);
22
+ }
12
23
  var pkg = JSON.parse(
13
24
  fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8")
14
25
  );
@@ -28,7 +39,7 @@ program.command("serve", { isDefault: true }).description("Start the Ocean Brain
28
39
  process.env.PORT = process.env.PORT || opts.port;
29
40
  process.env.HOST = process.env.HOST || opts.host;
30
41
  const schemaPath = path.resolve(serverRoot, "prisma/schema.prisma");
31
- const prisma = path.resolve(__dirname, "..", "node_modules", ".bin", "prisma");
42
+ const prisma = findBin("prisma", __dirname);
32
43
  execSync(`"${prisma}" generate --schema="${schemaPath}"`, {
33
44
  stdio: "inherit",
34
45
  env: { ...process.env }
@@ -37,6 +48,11 @@ program.command("serve", { isDefault: true }).description("Start the Ocean Brain
37
48
  stdio: "inherit",
38
49
  env: { ...process.env }
39
50
  });
40
- await import(path.resolve(serverRoot, "dist/main.js"));
51
+ const serverEntry = pathToFileURL(path.resolve(serverRoot, "dist/main.js")).href;
52
+ await import(serverEntry);
53
+ });
54
+ program.command("mcp").description("Start MCP server for AI integration").option("-s, --server <url>", "Ocean Brain server URL", "http://localhost:6683").action(async (opts) => {
55
+ const { startMcpServer } = await import("./mcp.js");
56
+ await startMcpServer(opts.server);
41
57
  });
42
58
  program.parse();
package/dist/mcp.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ var pkg = JSON.parse(
12
+ fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8")
13
+ );
14
+ async function graphql(serverUrl, query, variables) {
15
+ const response = await fetch(`${serverUrl}/graphql`, {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({ query, variables })
19
+ });
20
+ if (!response.ok) {
21
+ throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
22
+ }
23
+ const result = await response.json();
24
+ if (result.errors?.length) {
25
+ throw new Error(`GraphQL error: ${result.errors[0].message}`);
26
+ }
27
+ return result.data;
28
+ }
29
+ async function startMcpServer(serverUrl) {
30
+ const server = new McpServer({
31
+ name: "ocean-brain",
32
+ version: pkg.version
33
+ });
34
+ server.tool(
35
+ "search_notes",
36
+ "Search notes by keyword. Returns a list of matching notes with title and tags. Use read_note to get full content of a specific note.",
37
+ {
38
+ query: z.string().describe("Search keyword"),
39
+ limit: z.number().optional().default(10).describe("Max results (default: 10)")
40
+ },
41
+ async ({ query, limit }) => {
42
+ const data = await graphql(serverUrl, `
43
+ query ($searchFilter: SearchFilterInput, $pagination: PaginationInput) {
44
+ allNotes(searchFilter: $searchFilter, pagination: $pagination) {
45
+ totalCount
46
+ notes {
47
+ id
48
+ title
49
+ updatedAt
50
+ tags { id name }
51
+ contentAsMarkdown
52
+ }
53
+ }
54
+ }
55
+ `, {
56
+ searchFilter: { query, sortBy: "updatedAt", sortOrder: "desc" },
57
+ pagination: { limit, offset: 0 }
58
+ });
59
+ const result = data?.allNotes;
60
+ const notes = result.notes.map((note) => ({
61
+ id: note.id,
62
+ title: note.title,
63
+ updatedAt: note.updatedAt,
64
+ tags: note.tags.map((t) => t.name),
65
+ preview: note.contentAsMarkdown.slice(0, 100)
66
+ }));
67
+ return {
68
+ content: [{
69
+ type: "text",
70
+ text: JSON.stringify({ totalCount: result.totalCount, notes }, null, 2)
71
+ }]
72
+ };
73
+ }
74
+ );
75
+ server.tool(
76
+ "read_note",
77
+ "Read content of a note by ID. Returns truncated by default (1000 chars). Set maxLength to 0 for full content only when necessary.",
78
+ {
79
+ id: z.string().describe("Note ID"),
80
+ maxLength: z.number().optional().default(1e3).describe("Max content length in characters. 0 for full content. (default: 1000)")
81
+ },
82
+ async ({ id, maxLength }) => {
83
+ const data = await graphql(serverUrl, `
84
+ query ($id: ID!) {
85
+ note(id: $id) {
86
+ id
87
+ title
88
+ contentAsMarkdown
89
+ createdAt
90
+ updatedAt
91
+ tags { id name }
92
+ }
93
+ }
94
+ `, { id });
95
+ const note = data?.note;
96
+ let markdown = note.contentAsMarkdown;
97
+ const totalLength = markdown.length;
98
+ let truncated = false;
99
+ if (maxLength > 0 && markdown.length > maxLength) {
100
+ markdown = markdown.slice(0, maxLength);
101
+ truncated = true;
102
+ }
103
+ const output = [
104
+ `# ${note.title}`,
105
+ "",
106
+ `Tags: ${note.tags.map((t) => t.name).join(", ") || "(none)"}`,
107
+ `Created: ${note.createdAt}`,
108
+ `Updated: ${note.updatedAt}`,
109
+ ...truncated ? [`Content: ${totalLength} chars (showing first ${maxLength})`] : [],
110
+ "",
111
+ markdown,
112
+ ...truncated ? ["\n... (truncated, use maxLength: 0 to read full content)"] : []
113
+ ].join("\n");
114
+ return {
115
+ content: [{
116
+ type: "text",
117
+ text: output
118
+ }]
119
+ };
120
+ }
121
+ );
122
+ server.tool(
123
+ "list_tags",
124
+ "List all tags with their note counts.",
125
+ {},
126
+ async () => {
127
+ const data = await graphql(serverUrl, `
128
+ query ($searchFilter: SearchFilterInput, $pagination: PaginationInput) {
129
+ allTags(searchFilter: $searchFilter, pagination: $pagination) {
130
+ totalCount
131
+ tags {
132
+ id
133
+ name
134
+ referenceCount
135
+ }
136
+ }
137
+ }
138
+ `, {
139
+ searchFilter: { query: "" },
140
+ pagination: { limit: 100, offset: 0 }
141
+ });
142
+ const result = data?.allTags;
143
+ return {
144
+ content: [{
145
+ type: "text",
146
+ text: JSON.stringify(result, null, 2)
147
+ }]
148
+ };
149
+ }
150
+ );
151
+ server.tool(
152
+ "list_recent_notes",
153
+ "List recently updated notes. Returns titles and tags only. Use read_note to get content of a specific note.",
154
+ {
155
+ limit: z.number().optional().default(10).describe("Max results (default: 10)")
156
+ },
157
+ async ({ limit }) => {
158
+ const data = await graphql(serverUrl, `
159
+ query ($searchFilter: SearchFilterInput, $pagination: PaginationInput) {
160
+ allNotes(searchFilter: $searchFilter, pagination: $pagination) {
161
+ totalCount
162
+ notes {
163
+ id
164
+ title
165
+ updatedAt
166
+ tags { id name }
167
+ }
168
+ }
169
+ }
170
+ `, {
171
+ searchFilter: { query: "", sortBy: "updatedAt", sortOrder: "desc" },
172
+ pagination: { limit, offset: 0 }
173
+ });
174
+ const result = data?.allNotes;
175
+ const notes = result.notes.map((note) => ({
176
+ id: note.id,
177
+ title: note.title,
178
+ updatedAt: note.updatedAt,
179
+ tags: note.tags.map((t) => t.name)
180
+ }));
181
+ return {
182
+ content: [{
183
+ type: "text",
184
+ text: JSON.stringify({ totalCount: result.totalCount, notes }, null, 2)
185
+ }]
186
+ };
187
+ }
188
+ );
189
+ const transport = new StdioServerTransport();
190
+ await server.connect(transport);
191
+ }
192
+ export {
193
+ startMcpServer
194
+ };
package/package.json CHANGED
@@ -1,33 +1,43 @@
1
1
  {
2
- "name": "ocean-brain",
3
- "version": "0.1.6",
4
- "type": "module",
5
- "bin": {
6
- "ocean-brain": "./dist/index.js"
7
- },
8
- "files": [
9
- "dist",
10
- "server/dist",
11
- "server/prisma/schema.prisma",
12
- "server/prisma/migrations",
13
- "server/client/dist"
14
- ],
15
- "dependencies": {
16
- "@graphql-tools/schema": "^10.0.31",
17
- "@graphql-tools/utils": "^11.0.0",
18
- "@prisma/client": "^6.19.2",
19
- "commander": "^14.0.3",
20
- "express": "^5.2.1",
21
- "express-session": "^1.19.0",
22
- "express-winston": "^4.2.0",
23
- "graphql-http": "^1.22.4",
24
- "prisma": "^6.19.2",
25
- "sqlite3": "^5.1.7",
26
- "winston": "^3.19.0"
27
- },
28
- "scripts": {
29
- "dev": "tsx src/index.ts",
30
- "build": "tsup",
31
- "type-check": "tsc --noEmit"
32
- }
33
- }
2
+ "name": "ocean-brain",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/baealex/ocean-brain"
8
+ },
9
+ "homepage": "https://github.com/baealex/ocean-brain",
10
+ "bugs": {
11
+ "url": "https://github.com/baealex/ocean-brain/issues"
12
+ },
13
+ "bin": {
14
+ "ocean-brain": "dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "server/dist",
19
+ "server/prisma/schema.prisma",
20
+ "server/prisma/migrations",
21
+ "server/client/dist"
22
+ ],
23
+ "scripts": {
24
+ "dev": "tsx src/index.ts",
25
+ "build": "tsup",
26
+ "type-check": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@graphql-tools/schema": "^10.0.31",
30
+ "@graphql-tools/utils": "^11.0.0",
31
+ "@modelcontextprotocol/sdk": "^1.27.1",
32
+ "@prisma/client": "^6.19.2",
33
+ "commander": "^14.0.3",
34
+ "express": "^5.2.1",
35
+ "express-session": "^1.19.0",
36
+ "express-winston": "^4.2.0",
37
+ "graphql-http": "^1.22.4",
38
+ "prisma": "^6.19.2",
39
+ "sqlite3": "^5.1.7",
40
+ "winston": "^3.19.0",
41
+ "zod": "^3.25.0"
42
+ }
43
+ }