Compare commits

..

3 Commits

Author SHA1 Message Date
samanhappy
7f33615161 feat: support Streamable HTTP transport for downstream (#32) 2025-04-27 13:55:25 +08:00
samanhappy
c9ec3b77ce fix: sort market servers by official status (#36) 2025-04-26 21:17:05 +08:00
samanhappy
142c3f628a Update group chat invitation details (#34) 2025-04-25 10:00:56 +08:00
9 changed files with 124 additions and 29 deletions

View File

@@ -136,9 +136,9 @@ pnpm dev
- Bug 报告与修复
- 翻译与建议
欢迎加入企微交流共建群
欢迎加入企微交流共建群,由于群人数限制,有兴趣的同学可以扫码添加管理员为好友后拉入群聊。
<img src="assets/wegroup.png" width="500">
<img src="assets/wexin.png" width="350">
## 📄 许可证

BIN
assets/wexin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Server } from '@/types';
import ServerCard from '@/components/ServerCard';
import AddServerForm from '@/components/AddServerForm';
@@ -8,6 +9,7 @@ import { useServerData } from '@/hooks/useServerData';
const ServersPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const {
servers,
error,
@@ -50,6 +52,15 @@ const ServersPage: React.FC = () => {
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">{t('pages.servers.title')}</h1>
<div className="flex space-x-4">
<button
onClick={() => navigate('/market')}
className="px-4 py-2 bg-emerald-100 text-emerald-800 rounded hover:bg-emerald-200 flex items-center"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3z" />
</svg>
{t('nav.market')}
</button>
<AddServerForm onAdd={handleServerAdd} />
<button
onClick={handleRefresh}

View File

@@ -23,7 +23,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.9.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@shadcn/ui": "^0.0.4",

10
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.9.0
version: 1.9.0
specifier: ^1.10.2
version: 1.10.2
'@radix-ui/react-accordion':
specifier: ^1.2.3
version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -867,8 +867,8 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@modelcontextprotocol/sdk@1.9.0':
resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==}
'@modelcontextprotocol/sdk@1.10.2':
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
engines: {node: '>=18'}
'@next/env@15.2.4':
@@ -4268,7 +4268,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@modelcontextprotocol/sdk@1.9.0':
'@modelcontextprotocol/sdk@1.10.2':
dependencies:
content-type: 1.0.5
cors: 2.8.5

View File

@@ -4,7 +4,12 @@ import path from 'path';
import { initMcpServer } from './services/mcpService.js';
import { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js';
import { handleSseConnection, handleSseMessage } from './services/sseService.js';
import {
handleSseConnection,
handleSseMessage,
handleMcpPostRequest,
handleMcpOtherRequest,
} from './services/sseService.js';
import { migrateUserData } from './utils/migration.js';
import { initializeDefaultUser } from './models/User.js';
@@ -34,6 +39,9 @@ export class AppServer {
console.log('MCP server initialized successfully');
this.app.get('/sse/:group?', (req, res) => handleSseConnection(req, res));
this.app.post('/messages', handleSseMessage);
this.app.post('/mcp/:group?', handleMcpPostRequest);
this.app.get('/mcp/:group?', handleMcpOtherRequest);
this.app.delete('/mcp/:group?', handleMcpOtherRequest);
})
.catch((error) => {
console.error('Error initializing MCP server:', error);

View File

@@ -12,7 +12,15 @@ export const getMarketServers = (): Record<string, MarketServer> => {
try {
const serversJsonPath = getServersJsonPath();
const data = fs.readFileSync(serversJsonPath, 'utf8');
return JSON.parse(data);
const serversObj = JSON.parse(data) as Record<string, MarketServer>;
const sortedEntries = Object.entries(serversObj).sort(([, serverA], [, serverB]) => {
if (serverA.is_official && !serverB.is_official) return -1;
if (!serverA.is_official && serverB.is_official) return 1;
return 0;
});
return Object.fromEntries(sortedEntries);
} catch (error) {
console.error('Failed to load servers from servers.json:', error);
return {};
@@ -29,13 +37,13 @@ export const getMarketServerByName = (name: string): MarketServer | null => {
export const getMarketCategories = (): string[] => {
const servers = getMarketServers();
const categories = new Set<string>();
Object.values(servers).forEach((server) => {
server.categories?.forEach((category) => {
categories.add(category);
});
});
return Array.from(categories).sort();
};
@@ -43,25 +51,28 @@ export const getMarketCategories = (): string[] => {
export const getMarketTags = (): string[] => {
const servers = getMarketServers();
const tags = new Set<string>();
Object.values(servers).forEach((server) => {
server.tags?.forEach((tag) => {
tags.add(tag);
});
});
return Array.from(tags).sort();
};
// Search market servers by query
export const searchMarketServers = (query: string): MarketServer[] => {
const servers = getMarketServers();
const searchTerms = query.toLowerCase().split(' ').filter(term => term.length > 0);
const searchTerms = query
.toLowerCase()
.split(' ')
.filter((term) => term.length > 0);
if (searchTerms.length === 0) {
return Object.values(servers);
}
return Object.values(servers).filter((server) => {
// Search in name, display_name, description, categories, and tags
const searchableText = [
@@ -69,21 +80,23 @@ export const searchMarketServers = (query: string): MarketServer[] => {
server.display_name,
server.description,
...(server.categories || []),
...(server.tags || [])
].join(' ').toLowerCase();
return searchTerms.some(term => searchableText.includes(term));
...(server.tags || []),
]
.join(' ')
.toLowerCase();
return searchTerms.some((term) => searchableText.includes(term));
});
};
// Filter market servers by category
export const filterMarketServersByCategory = (category: string): MarketServer[] => {
const servers = getMarketServers();
if (!category) {
return Object.values(servers);
}
return Object.values(servers).filter((server) => {
return server.categories?.includes(category);
});
@@ -92,12 +105,12 @@ export const filterMarketServersByCategory = (category: string): MarketServer[]
// Filter market servers by tag
export const filterMarketServersByTag = (tag: string): MarketServer[] => {
const servers = getMarketServers();
if (!tag) {
return Object.values(servers);
}
return Object.values(servers).filter((server) => {
return server.tags?.includes(tag);
});
};
};

View File

@@ -29,7 +29,7 @@ export const notifyToolChanged = async () => {
currentServer
.sendToolListChanged()
.catch((error) => {
console.error('Failed to send tool list changed notification:', error);
console.warn('Failed to send tool list changed notification:', error.message);
})
.then(() => {
console.log('Tool list changed notification sent successfully');

View File

@@ -1,9 +1,13 @@
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { getMcpServer } from './mcpService.js';
import { loadSettings } from '../config/index.js';
const transports: { [sessionId: string]: { transport: SSEServerTransport; group: string } } = {};
const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
export const getGroup = (sessionId: string): string => {
return transports[sessionId]?.group || '';
@@ -44,13 +48,72 @@ export const handleSseMessage = async (req: Request, res: Response): Promise<voi
req.query.group = group;
console.log(`Received message for sessionId: ${sessionId} in group: ${group}`);
if (transport) {
await transport.handlePostMessage(req, res);
await (transport as SSEServerTransport).handlePostMessage(req, res);
} else {
console.error(`No transport found for sessionId: ${sessionId}`);
res.status(400).send('No transport found for sessionId');
}
};
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
console.log('Handling MCP post request');
const sessionId = req.headers['mcp-session-id'] as string | undefined;
const group = req.params.group;
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
enableGlobalRoute: true,
enableGroupNameRoute: true,
};
if (!group && !routingConfig.enableGlobalRoute) {
res.status(403).send('Global routes are disabled. Please specify a group ID.');
return;
}
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId].transport as StreamableHTTPServerTransport;
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports[sessionId] = { transport, group };
},
});
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
await getMcpServer().connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
await transport.handleRequest(req, res, req.body);
};
export const handleMcpOtherRequest = async (req: Request, res: Response) => {
console.log('Handling MCP other request');
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 as StreamableHTTPServerTransport).handleRequest(req, res);
};
export const getConnectionCount = (): number => {
return Object.keys(transports).length;
};