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 +19 -3
- package/dist/mcp.js +194 -0
- package/package.json +42 -32
- package/server/client/dist/assets/index-BJgBFWwL.css +1 -0
- package/server/client/dist/assets/index-DvNRv9YC.js +391 -0
- package/server/client/dist/assets/{note-vendor-DhbUD4OO.js → note-vendor-CUVHa9LU.js} +1 -1
- package/server/client/dist/index.html +3 -3
- package/server/dist/modules/blocknote.js +46 -0
- package/server/dist/modules/blocknote.js.map +1 -0
- package/server/dist/schema/note/index.js +8 -1
- package/server/dist/schema/note/index.js.map +1 -1
- package/server/client/dist/assets/index-BBmw_0Bo.css +0 -1
- package/server/client/dist/assets/index-TKn2sngc.js +0 -393
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|