Skip to content

Commit 0e7b2e8

Browse files
committed
Add MCP server
1 parent be2b6bf commit 0e7b2e8

File tree

6 files changed

+736
-3
lines changed

6 files changed

+736
-3
lines changed

build_bundle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const result = await esbuild.build({
1818
treeShaking: true,
1919
logLevel: "error",
2020
minify: true,
21-
external: [],
21+
external: ["./server/mcp_server.ts"],
2222
plugins: denoPlugins({
2323
configPath: new URL("./deno.json", import.meta.url).pathname,
2424
nodeModulesDir: "auto",

cmd/server.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HttpServer } from "../server/http_server.ts";
1+
import { HttpServer, type McpServerOptions } from "../server/http_server.ts";
22

33
// To ship silverbullet as a single binary, we're importing all assets (client and plugs) as JSON blobs
44
import clientAssetBundle from "../dist/client_asset_bundle.json" with {
@@ -107,6 +107,21 @@ export async function serveCommand(
107107
const shellBackend = Deno.env.get("SB_SHELL_BACKEND") || "local";
108108
const spaceIgnore = Deno.env.get("SB_SPACE_IGNORE");
109109

110+
// MCP server configuration
111+
const mcpEnabled = Deno.env.get("SB_MCP_ENABLED") === "true";
112+
const mcpAuthMode = (Deno.env.get("SB_MCP_AUTH_MODE") as "inherit" | "separate" | "none") || "inherit";
113+
const mcpAuthToken = Deno.env.get("SB_MCP_AUTH_TOKEN");
114+
115+
let mcpOptions: McpServerOptions | undefined;
116+
if (mcpEnabled) {
117+
mcpOptions = {
118+
enabled: true,
119+
authMode: mcpAuthMode,
120+
authToken: mcpAuthToken,
121+
};
122+
console.log(`MCP server enabled with auth mode: ${mcpAuthMode}`);
123+
}
124+
110125
// All plug code bundled into a JSON blob
111126
const plugAssets = new AssetBundle(plugAssetBundle as AssetJson);
112127
// All client files bundled into a JSON blob
@@ -127,6 +142,7 @@ export async function serveCommand(
127142
readOnly,
128143
shellBackend,
129144
pagesPath: folder,
145+
mcp: mcpOptions,
130146
},
131147
clientAssets,
132148
plugAssets,
@@ -136,6 +152,17 @@ export async function serveCommand(
136152
// And kick it off
137153
await httpServer.start();
138154

155+
// Handle graceful shutdown
156+
const shutdown = async () => {
157+
console.log("Shutting down SilverBullet...");
158+
await httpServer.stop();
159+
Deno.exit(0);
160+
};
161+
162+
// Set up signal handlers for graceful shutdown
163+
Deno.addSignalListener("SIGINT", shutdown);
164+
Deno.addSignalListener("SIGTERM", shutdown);
165+
139166
// Wait in an infinite loop (to keep the HTTP server running, only cancelable via Ctrl+C or other signal)
140167
while (true) {
141168
await sleep(10000);

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"imports": {
9898
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7",
9999
"@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.7",
100+
"@phughesmcr/deno-mcp-template": "jsr:@phughesmcr/deno-mcp-template@^0.1.0",
100101
"@silverbulletmd/silverbullet/syscall": "./plug-api/syscall.ts",
101102
"@silverbulletmd/silverbullet/syscalls": "./plug-api/syscalls.ts",
102103
"@silverbulletmd/silverbullet/type/client": "./type/client.ts",

deno.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/http_server.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ import {
2626
import { CONFIG_TEMPLATE, INDEX_TEMPLATE } from "../web/PAGE_TEMPLATES.ts";
2727
import type { FileMeta } from "../type/index.ts";
2828

29+
// Conditionally import MCP types only (not implementation)
30+
export type McpServerOptions = {
31+
enabled: boolean;
32+
authMode: "inherit" | "separate" | "none";
33+
authToken?: string;
34+
};
35+
2936
const authenticationExpirySeconds = 60 * 60 * 24 * 7; // 1 week
3037

3138
export type ServerOptions = {
@@ -38,6 +45,7 @@ export type ServerOptions = {
3845
shellBackend: string;
3946
readOnly: boolean;
4047
indexPage: string;
48+
mcp?: McpServerOptions;
4149
};
4250

4351
export class HttpServer {
@@ -48,6 +56,7 @@ export class HttpServer {
4856
private readonly spacePrimitives: SpacePrimitives;
4957
private jwtIssuer: JWTIssuer;
5058
private readonly shellBackend: ShellBackend;
59+
private mcpServerManager?: any;
5160

5261
constructor(
5362
readonly options: ServerOptions,
@@ -79,6 +88,19 @@ export class HttpServer {
7988
? new NotSupportedShell() // No shell for read only mode
8089
: determineShellBackend(options);
8190
this.jwtIssuer = new JWTIssuer(baseKvPrimitives);
91+
92+
// MCP server will be initialized in start() method
93+
}
94+
95+
private async initializeMcpServer() {
96+
try {
97+
// Dynamic import to avoid bundling MCP dependencies
98+
const { McpServerManager } = await import("./mcp_server.ts");
99+
this.mcpServerManager = new McpServerManager(this.spacePrimitives, this.options.mcp!);
100+
} catch (error) {
101+
console.error("Failed to initialize MCP server:", error);
102+
console.error("MCP functionality will not be available");
103+
}
82104
}
83105

84106
async start() {
@@ -88,10 +110,17 @@ export class HttpServer {
88110
JSON.stringify({ auth: this.options.auth }),
89111
);
90112
}
113+
114+
// Initialize MCP server if enabled
115+
if (this.options.mcp?.enabled) {
116+
await this.initializeMcpServer();
117+
}
118+
91119
await this.ensureBasicPages();
92120
// Serve static files (javascript, css, html)
93121
this.serveStatic();
94122
this.addAuth();
123+
this.addMcpRoutes();
95124
this.addFsRoutes();
96125

97126
// Fallback, serve the UI index.html
@@ -175,7 +204,10 @@ export class HttpServer {
175204
});
176205
}
177206

178-
stop() {
207+
async stop() {
208+
if (this.mcpServerManager) {
209+
await this.mcpServerManager.shutdown();
210+
}
179211
if (this.abortController) {
180212
this.abortController.abort();
181213
console.log("stopped server");
@@ -439,6 +471,38 @@ export class HttpServer {
439471
);
440472
}
441473

474+
private addMcpRoutes() {
475+
if (!this.mcpServerManager) {
476+
return;
477+
}
478+
479+
// MCP endpoint - handles all MCP communication
480+
this.app.all("/mcp", async (c) => {
481+
return await this.mcpServerManager!.handleMcpRequest(c);
482+
});
483+
484+
// MCP info endpoint (no auth required for basic info)
485+
this.app.get("/.mcp", (c) => {
486+
if (!this.options.mcp?.enabled) {
487+
return c.json({
488+
mcp: {
489+
enabled: false,
490+
message: "MCP server is disabled"
491+
}
492+
});
493+
}
494+
495+
return c.json({
496+
mcp: {
497+
enabled: true,
498+
authMode: this.options.mcp.authMode,
499+
endpoint: "/mcp",
500+
version: "2.0.0",
501+
},
502+
});
503+
});
504+
}
505+
442506
private refreshLogin(c: Context, host: string) {
443507
if (getCookie(c, "refreshLogin")) {
444508
const inAWeek = new Date(

0 commit comments

Comments
 (0)