[WIP] Refactor everything server to be more modular and use recommended APIs.

Finalized Roots list changed handling and initial request. Final fit and finish work.

* Updated architecture.md
  - Added links to other docs
  - Refactor/extracted sections into extension.md, features.md, how-it-works.md, startup.md, and structure.md

* Removed everything.ts
  - all features are ported

* In roots.ts
  - refactor/renaned setRootsListChangedHandler to syncRoots
  - refactor handler logic to requestRoots function
  - Calls for roots list directly to get initial list

* In server/index.ts
  - import setRootsListChangedHandler
  - in clientConnected callback
    - call setRootsListChangedHandler passing server and sessionId

* In sse.ts, stdio.ts, and streamableHttp.ts
  - update inline and function docs

* In index.ts,
  - updated usage output

* In server/index.ts
  - refactor/extracted readInstructions to resources/index.ts
  - defined ServerFactoryResponse response type
This commit is contained in:
cliffhall 2025-12-12 20:12:18 -05:00
commit 9b25a3a41b
3 changed files with 581 additions and 1533 deletions

View file

@ -5,6 +5,7 @@ import cors from "cors";
console.error("Starting SSE server...");
// Express app with permissive CORS for testing with Inspector direct connect mode
const app = express();
app.use(
cors({
@ -13,16 +14,20 @@ app.use(
preflightContinue: false,
optionsSuccessStatus: 204,
})
); // Enable CORS for all routes so Inspector can connect
);
// Map sessionId to transport for each client
const transports: Map<string, SSEServerTransport> = new Map<
string,
SSEServerTransport
>();
// Handle GET requests for new SSE streams
app.get("/sse", async (req, res) => {
let transport: SSEServerTransport;
const { server, clientConnected, cleanup } = createServer();
// Session Id should not exist for GET /sse requests
if (req?.query?.sessionId) {
const sessionId = req?.query?.sessionId as string;
transport = transports.get(sessionId) as SSEServerTransport;
@ -31,15 +36,14 @@ app.get("/sse", async (req, res) => {
transport.sessionId
);
} else {
// Create and store transport for new session
// Create and store transport for the new session
transport = new SSEServerTransport("/message", res);
transports.set(transport.sessionId, transport);
// Connect server to transport
// Connect server to transport and invoke clientConnected callback
await server.connect(transport);
const sessionId = transport.sessionId;
clientConnected(sessionId);
console.error("Client Connected: ", sessionId);
// Handle close of connection
@ -47,13 +51,17 @@ app.get("/sse", async (req, res) => {
const sessionId = transport.sessionId;
console.error("Client Disconnected: ", sessionId);
transports.delete(sessionId);
await cleanup(sessionId);
cleanup(sessionId);
};
}
});
// Handle POST requests for client messages
app.post("/message", async (req, res) => {
// Session Id should exist for POST /message requests
const sessionId = req?.query?.sessionId as string;
// Get the transport for this session and use it to handle the request
const transport = transports.get(sessionId);
if (transport) {
console.error("Client Message from", sessionId);
@ -63,6 +71,7 @@ app.post("/message", async (req, res) => {
}
});
// Start the express server
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.error(`Server is running on port ${PORT}`);

View file

@ -5,12 +5,22 @@ import { createServer } from "../server/index.js";
console.error("Starting default (STDIO) server...");
async function main() {
/**
* The main method
* - Initializes the StdioServerTransport, sets up the server,
* - Connects the transport to the server, invokes the `clientConnected` callback,
* - Handles cleanup on process exit.
*
* @return {Promise<void>} A promise that resolves when the main function has executed and the process exits.
*/
async function main(): Promise<void> {
const transport = new StdioServerTransport();
const { server, clientConnected, cleanup } = createServer();
// Connect transport to server and invoke clientConnected callback
await server.connect(transport);
clientConnected();
// Cleanup on exit
process.on("SIGINT", async () => {
await server.close();

View file

@ -7,6 +7,7 @@ import cors from "cors";
console.log("Starting Streamable HTTP server...");
// Express app with permissive CORS for testing with Inspector direct connect mode
const app = express();
app.use(
cors({
@ -16,13 +17,15 @@ app.use(
optionsSuccessStatus: 204,
exposedHeaders: ["mcp-session-id", "last-event-id", "mcp-protocol-version"],
})
); // Enable CORS for all routes so Inspector can connect
);
// Map sessionId to server transport for each client
const transports: Map<string, StreamableHTTPServerTransport> = new Map<
string,
StreamableHTTPServerTransport
>();
// Handle POST requests for client messages
app.post("/mcp", async (req: Request, res: Response) => {
console.log("Received MCP POST request");
try {
@ -47,7 +50,6 @@ app.post("/mcp", async (req: Request, res: Response) => {
// This avoids race conditions where requests might come in before the session is stored
console.log(`Session initialized with ID: ${sessionId}`);
transports.set(sessionId, transport);
clientConnected(sessionId);
},
});
@ -66,9 +68,8 @@ app.post("/mcp", async (req: Request, res: Response) => {
// Connect the transport to the MCP server BEFORE handling the request
// so responses can flow back through the same transport
await server.connect(transport);
clientConnected(transport.sessionId);
await transport.handleRequest(req, res);
return;
} else {
// Invalid request - no session ID or not initialization request
@ -102,7 +103,7 @@ app.post("/mcp", async (req: Request, res: Response) => {
}
});
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
// Handle GET requests for SSE streams
app.get("/mcp", async (req: Request, res: Response) => {
console.log("Received MCP GET request");
const sessionId = req.headers["mcp-session-id"] as string | undefined;
@ -130,7 +131,7 @@ app.get("/mcp", async (req: Request, res: Response) => {
await transport!.handleRequest(req, res);
});
// Handle DELETE requests for session termination (according to MCP spec)
// Handle DELETE requests for session termination
app.delete("/mcp", async (req: Request, res: Response) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
@ -172,6 +173,7 @@ const server = app.listen(PORT, () => {
console.error(`MCP Streamable HTTP Server listening on port ${PORT}`);
});
// Handle server errors
server.on("error", (err: unknown) => {
const code =
typeof err === "object" && err !== null && "code" in err