app.ts•5.29 kB
import express from 'express'
import type { Request, Response, NextFunction } from 'express'
import { join } from 'path'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import {
getSiteMap,
getDocContentByUri,
handleMdxContent,
} from './utils/mdx.js'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
import { randomUUID } from 'node:crypto'
import cors from 'cors'
const DOCS_DIST_DIR = join(process.cwd(), '.docs')
export const createApp = (server: McpServer) => {
const app = express()
app.use(cors())
app.use(express.json())
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}
app.get('/', (_req: Request, res: Response) => {
res.send('Hello, this is the MCP server!')
})
app.get('/health', (_req: Request, res: Response) => {
res.json({
status: 'ok',
message: 'MCP server is running',
time: new Date().toISOString(),
})
})
app.get('/sitemap', async (_req: Request, res: Response) => {
const siteMapFile = join(DOCS_DIST_DIR, 'sitemap.xml')
const siteMapContent = await getSiteMap(siteMapFile)
console.log('siteMapContent', siteMapContent)
const siteMap = siteMapContent.map((item) => ({
uri: `docs://${item.loc}`,
text: `Documentation: ${item.loc}`,
}))
res.json({
contents: siteMap,
siteMapFile,
})
})
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req: Request, res: Response) => {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined
let transport: StreamableHTTPServerTransport
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId]
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports[sessionId] = transport
},
})
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId]
}
}
// const server = new McpServer({
// name: 'example-server',
// version: '1.0.0',
// })
// ... set up server resources, tools, and prompts ...
// Connect to the MCP server
await server.connect(transport)
} else {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
})
return
}
// Handle the request
await transport.handleRequest(req, res, req.body)
})
// Reusable handler for GET and DELETE requests
const handleSessionRequest = async (
req: express.Request,
res: express.Response
) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID')
return
}
const transport = transports[sessionId]
await transport.handleRequest(req, res)
}
// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest)
// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest)
// 添加获取文档内容接口 - 使用中间件方式
const docContentMiddleware = async (
_req: Request,
res: Response,
next: NextFunction
) => {
const uri = _req.query.uri as string
const isPretty = _req.query.isPretty as string
if (!uri) {
return res.status(400).json({ error: 'URI参数必须提供' })
}
try {
const rawContent = await getDocContentByUri(uri)
const {
content,
metadata: { frontmatter },
} = handleMdxContent(rawContent)
// res.json({
// uri: uri,
// text: content,
// })
console.log('uri', uri, isPretty)
if (isPretty) {
const escapedContent = content
.replace(/</g, '<')
.replace(/>/g, '>')
const html = `
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body>
<pre style="word-wrap: break-word; white-space: pre-wrap;">${escapedContent}</pre>
</body>
</html>
`
return res.send(html)
}
res.send(content)
} catch (error) {
console.error(`获取文档内容出错。URI: ${uri}`, error)
res.status(500).json({ error: '获取文档内容失败' })
}
}
// @ts-ignore Express 5类型定义问题
app.use('/doc-content-by-uri', docContentMiddleware)
return app
}