Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import { runServer, restartServer } from "./modules/server";
export type MCPPluginOptions = {
port: number;
specPath?: string; // OpenAPI specification path
serverName?: string; // MCP server name for discovery
serverVersion?: string; // MCP server version for discovery
toolSearchName?: string; // Name of the search tool
toolSearchDescription?: string; // Description of the search tool
};

let serverBootFlg = false;

export function MCPPlugin(inlineOptions?: Partial<MCPPluginOptions>): Plugin {
function MCPPlugin(inlineOptions?: Partial<MCPPluginOptions>): Plugin {
return {
name: "vitepress-plugin-mcp",
enforce: "post",
Expand Down Expand Up @@ -62,7 +66,8 @@ export function MCPPlugin(inlineOptions?: Partial<MCPPluginOptions>): Plugin {
return;
}

runServer(inlineOptions?.port);
const { port, specPath, ...serverOpts } = inlineOptions ?? {};
runServer(port, false, serverOpts);
}, 1500);
},
configurePreviewServer(server) {
Expand All @@ -77,3 +82,9 @@ export function MCPPlugin(inlineOptions?: Partial<MCPPluginOptions>): Plugin {
},
} satisfies Plugin;
}

export {
MCPPlugin,
runServer,
restartServer,
}
78 changes: 64 additions & 14 deletions src/modules/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,36 @@ const transports = {
streamable: {} as Record<string, StreamableHTTPServerTransport>,
sse: {} as Record<string, SSEServerTransport>,
};

export interface MCPServerOptions {
/** Server metadata shown in GET /mcp discovery response */
serverName: string;
serverVersion: string;
/** Search tool configuration */
toolSearchName: string;
toolSearchDescription: string;
}

let serverOptions: MCPServerOptions = {
serverName: "VitePress-server",
serverVersion: "1.0.0",
toolSearchName: "search_vitepress_docs",
toolSearchDescription:
"Search VitePress Documents For This Product. Extract up to five keywords each English and native language, and define all of them as single words. e.g. Vitepress, API, Specification,Extensions etc.",
};

let mcpServer = new McpServer({
name: "VitePress-server",
version: "1.0.0",
name: serverOptions.serverName,
version: serverOptions.serverVersion,
});

let app = express();

let appServer: Server;

export function runServer(port = 3000, buildMode = false) {
export function runServer(port = 3000, buildMode = false, options: Partial<MCPServerOptions> = {}) {
serverOptions = Object.assign(serverOptions, options);

console.log(
styleText(
"blue",
Expand All @@ -37,23 +57,47 @@ export function runServer(port = 3000, buildMode = false) {
app.use(express.json());

mcpServer = new McpServer({
name: "VitePress-server",
version: "1.0.0",
name: serverOptions.serverName,
version: serverOptions.serverVersion,
});

toolSearchVitePressDocs(mcpServer, buildMode);
toolSearchVitePressDocs(mcpServer, buildMode, serverOptions.toolSearchName, serverOptions.toolSearchDescription);
promptBasic(mcpServer);

// GET /mcp: SSE streaming (with session ID) or server discovery (without session ID)
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && transports.streamable[sessionId]) {
// Valid session: delegate to transport for SSE streaming
await callStreamableServer(req, res);
} else {
// No session: return server metadata JSON (discovery / health check)
res.json({
server: {
name: serverOptions.serverName,
version: serverOptions.serverVersion,
transport: "http",
},
capabilities: {
tools: serverOptions.toolSearchName
? { name: serverOptions.toolSearchName, description: serverOptions.toolSearchDescription }
: {},
},
});
}
});

// NOTE:Handle POST requests for client-to-server communication
app.post("/mcp", async (req, res) => {
console.log("POST request received at /mcp");
await callStreamableServer(req, res);
});

// // Handle GET requests for Streamable HTTP sessions
// app.get("/mcp", async (req, res) => {
// await callStreamableServer(req, res);
// });
// Handle DELETE requests for session termination (StreamableHTTP)
app.delete("/mcp", async (req, res) => {
console.log("DELETE request received at /mcp");
await callStreamableServer(req, res);
});

// Handle GET requests for server-to-client notifications via SSE
app.get("/mcp/__sse", handleSSESessionRequest);
Expand All @@ -80,8 +124,7 @@ export function runServer(port = 3000, buildMode = false) {
if ((errorMessage ?? "").includes("address already in use")) {
console.error("Error starting server:", error?.message);
console.warn("Port", port, "is already in use. Retrying with next port...");
port++;
runServer(port);
runServer(port + 1, buildMode, serverOptions);
return;
}
console.log(styleText("whiteBright", `VitePress Plugin MCP`));
Expand All @@ -106,7 +149,7 @@ const callStreamableServer = async (req: express.Request, res: express.Response)
if (sessionId && transports.streamable[sessionId]) {
// Reuse existing transport
transport = transports.streamable[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
} else if (!sessionId && req.body && isInitializeRequest(req.body)) {
console.log("Initializing new transport");
transport = await initializeStreamableTransport();

Expand Down Expand Up @@ -215,6 +258,13 @@ const initializeStreamableTransport = async () => {
*/
const initializeMCPServer = async (transport: StreamableHTTPServerTransport | SSEServerTransport, res: express.Response) => {
try {
// Close any existing transport before connecting a new one
// (MCP SDK only allows one active connection per server)
try {
await mcpServer.close();
} catch {
// Server may not be connected yet; that's fine
}
// Connect to the MCP server
await mcpServer.connect(transport);
} catch (error) {
Expand Down Expand Up @@ -242,7 +292,7 @@ async function initializeServers() {
}
}
for (const sessionId in transports.sse) {
const transport = transports.streamable[sessionId];
const transport = transports.sse[sessionId];
if (transport) {
await transport.close();
console.log(`Transport closed for session ID: ${sessionId}`);
Expand Down
6 changes: 3 additions & 3 deletions src/tools/searchVitePressDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { z } from "zod";
import { searchByMiniSearch } from "../modules/search/miniSearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function toolSearchVitePressDocs(mcp: McpServer, buildMode = false) {
export function toolSearchVitePressDocs(mcp: McpServer, buildMode = false, toolSearchName: string, toolSearchDescription: string) {
mcp.tool(
"search_vitepress_docs",
"Search VitePress Documents For This Product. Extract up to five keywords each English and native language, and define all of them as single words. e.g. Vitepress, API, Specification,Extensions etc.",
toolSearchName,
toolSearchDescription,
{ keywords: z.array(z.string()) },
async ({ keywords }) => {
console.log("START: search-docs");
Expand Down