silverbullet-ai-mcp 0.0.0
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 +173 -0
- package/package.json +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer as createHttpServer } from "node:http";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
const SB_URL = process.env.SB_URL;
|
|
8
|
+
if (!SB_URL) {
|
|
9
|
+
console.error("SB_URL environment variable is required");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const SB_AUTH_TOKEN = process.env.SB_AUTH_TOKEN;
|
|
13
|
+
const MCP_PORT = process.env.MCP_PORT;
|
|
14
|
+
const MCP_TOKEN = process.env.MCP_TOKEN;
|
|
15
|
+
const EXCLUDED_TOOLS = new Set(["ask_user", "navigate"]);
|
|
16
|
+
// Space Lua has no json.decode, so pass arguments as Lua literals.
|
|
17
|
+
// TODO: export this from sb or sbai?
|
|
18
|
+
function toLuaLiteral(value) {
|
|
19
|
+
if (value === null || value === undefined)
|
|
20
|
+
return "nil";
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
const escaped = value
|
|
23
|
+
.replace(/\\/g, "\\\\")
|
|
24
|
+
.replace(/"/g, '\\"')
|
|
25
|
+
.replace(/\n/g, "\\n")
|
|
26
|
+
.replace(/\r/g, "\\r")
|
|
27
|
+
.replace(/\t/g, "\\t");
|
|
28
|
+
return `"${escaped}"`;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
return Number.isFinite(value) ? String(value) : "nil";
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "boolean")
|
|
34
|
+
return value ? "true" : "false";
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return `{${value.map(toLuaLiteral).join(", ")}}`;
|
|
37
|
+
if (typeof value === "object") {
|
|
38
|
+
const pairs = Object.entries(value)
|
|
39
|
+
.map(([k, v]) => `[${toLuaLiteral(k)}]=${toLuaLiteral(v)}`)
|
|
40
|
+
.join(", ");
|
|
41
|
+
return `{${pairs}}`;
|
|
42
|
+
}
|
|
43
|
+
return "nil";
|
|
44
|
+
}
|
|
45
|
+
async function evalLuaScript(script) {
|
|
46
|
+
const headers = {
|
|
47
|
+
"Content-Type": "text/plain",
|
|
48
|
+
"X-Timeout": "60",
|
|
49
|
+
};
|
|
50
|
+
if (SB_AUTH_TOKEN)
|
|
51
|
+
headers["Authorization"] = `Bearer ${SB_AUTH_TOKEN}`;
|
|
52
|
+
const res = await fetch(`${SB_URL}/.runtime/lua_script`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers,
|
|
55
|
+
body: script,
|
|
56
|
+
});
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
let json = {};
|
|
59
|
+
try {
|
|
60
|
+
json = JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// invalid json or not json (e.g. "Unauthorized" from SB)
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok || json.error) {
|
|
66
|
+
throw new Error(json.error ??
|
|
67
|
+
`SilverBullet returned HTTP ${res.status} ${res.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
return json.result;
|
|
70
|
+
}
|
|
71
|
+
// Current tool set, refreshed at startup and via the refresh_tools tool.
|
|
72
|
+
let toolList = [];
|
|
73
|
+
async function refreshTools() {
|
|
74
|
+
const tools = (await evalLuaScript(`return system.invokeFunction("silverbullet-ai.listTools")`));
|
|
75
|
+
toolList = tools.filter((t) => t.source !== "mcp" && !EXCLUDED_TOOLS.has(t.name));
|
|
76
|
+
return toolList.length;
|
|
77
|
+
}
|
|
78
|
+
const REFRESH_TOOL = {
|
|
79
|
+
name: "refresh_tools",
|
|
80
|
+
description: "Reload the list of available tools from SilverBullet. Use this if tools were added or changed since this server started.",
|
|
81
|
+
inputSchema: { type: "object", properties: {} },
|
|
82
|
+
annotations: { readOnlyHint: true },
|
|
83
|
+
};
|
|
84
|
+
const errText = (e) => e instanceof Error ? e.message : String(e);
|
|
85
|
+
const ok = (text) => ({
|
|
86
|
+
content: [{ type: "text", text }],
|
|
87
|
+
isError: false,
|
|
88
|
+
});
|
|
89
|
+
const err = (text) => ({
|
|
90
|
+
content: [{ type: "text", text }],
|
|
91
|
+
isError: true,
|
|
92
|
+
});
|
|
93
|
+
function buildServer() {
|
|
94
|
+
const server = new Server({ name: "silverbullet-ai-mcp", version: "0.0.0" }, { capabilities: { tools: { listChanged: true } } });
|
|
95
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
96
|
+
tools: [
|
|
97
|
+
REFRESH_TOOL,
|
|
98
|
+
...toolList.map((t) => ({
|
|
99
|
+
name: t.name,
|
|
100
|
+
description: t.description,
|
|
101
|
+
inputSchema: t.parameters,
|
|
102
|
+
annotations: { readOnlyHint: t.readOnly === true },
|
|
103
|
+
})),
|
|
104
|
+
],
|
|
105
|
+
}));
|
|
106
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
107
|
+
const { name, arguments: args } = request.params;
|
|
108
|
+
if (name === REFRESH_TOOL.name) {
|
|
109
|
+
try {
|
|
110
|
+
const count = await refreshTools();
|
|
111
|
+
await server.sendToolListChanged();
|
|
112
|
+
return ok(`Reloaded ${count} tools.`);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return err(errText(e));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!toolList.some((t) => t.name === name)) {
|
|
119
|
+
return err(`Unknown tool: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const r = (await evalLuaScript(`return system.invokeFunction("silverbullet-ai.callTool", ${toLuaLiteral(name)}, ${toLuaLiteral(args ?? {})})`));
|
|
123
|
+
return r.success
|
|
124
|
+
? ok(r.result ?? "")
|
|
125
|
+
: err(r.error ?? "Tool call failed");
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return err(errText(e));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
await refreshTools();
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(`Failed to load tools from SilverBullet: ${errText(e)}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
console.error(`silverbullet-ai-mcp: exposing ${toolList.length} tools: ${toolList
|
|
141
|
+
.map((t) => t.name)
|
|
142
|
+
.join(", ")}`);
|
|
143
|
+
if (MCP_PORT) {
|
|
144
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
145
|
+
if (MCP_TOKEN && req.headers.authorization !== `Bearer ${MCP_TOKEN}`) {
|
|
146
|
+
res.writeHead(401).end();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const server = buildServer();
|
|
150
|
+
const transport = new StreamableHTTPServerTransport({
|
|
151
|
+
sessionIdGenerator: undefined,
|
|
152
|
+
});
|
|
153
|
+
res.on("close", () => {
|
|
154
|
+
transport.close();
|
|
155
|
+
server.close();
|
|
156
|
+
});
|
|
157
|
+
try {
|
|
158
|
+
await server.connect(transport);
|
|
159
|
+
await transport.handleRequest(req, res);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
console.error("Error handling request:", e);
|
|
163
|
+
if (!res.headersSent)
|
|
164
|
+
res.writeHead(500).end();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
httpServer.listen(Number(MCP_PORT), () => {
|
|
168
|
+
console.error(`silverbullet-ai-mcp listening on port ${MCP_PORT}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await buildServer().connect(new StdioServerTransport());
|
|
173
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "silverbullet-ai-mcp",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "MCP server bridge exposing silverbullet-ai tools from a SilverBullet server",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"silverbullet-ai-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.0.0",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
}
|
|
27
|
+
}
|