From 8ae542bdab19c3f09b2edfa53137d67d76e8f2e1 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Tue, 30 Dec 2025 18:45:33 +0800 Subject: [PATCH] Add server renaming functionality (#533) --- frontend/src/components/EditServerForm.tsx | 12 ++++- frontend/src/components/ServerForm.tsx | 1 - mcp_settings.json | 3 +- src/controllers/serverController.ts | 51 +++++++++++++++++++--- src/dao/BearerKeyDao.ts | 34 +++++++++++++++ src/dao/BearerKeyDaoDbImpl.ts | 26 +++++++++++ src/dao/GroupDao.ts | 40 +++++++++++++++++ src/dao/GroupDaoDbImpl.ts | 31 +++++++++++++ src/dao/ServerDao.ts | 30 +++++++++++-- src/dao/ServerDaoDbImpl.ts | 9 ++++ src/db/repositories/ServerRepository.ts | 13 ++++++ 11 files changed, 238 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/EditServerForm.tsx b/frontend/src/components/EditServerForm.tsx index bb46dc5..884bbe5 100644 --- a/frontend/src/components/EditServerForm.tsx +++ b/frontend/src/components/EditServerForm.tsx @@ -18,7 +18,17 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => { try { setError(null); const encodedServerName = encodeURIComponent(server.name); - const result = await apiPut(`/servers/${encodedServerName}`, payload); + + // Check if name is being changed + const isRenaming = payload.name && payload.name !== server.name; + + // Build the request body + const requestBody = { + config: payload.config, + ...(isRenaming ? { newName: payload.name } : {}), + }; + + const result = await apiPut(`/servers/${encodedServerName}`, requestBody); if (!result.success) { // Use specific error message from the response if available diff --git a/frontend/src/components/ServerForm.tsx b/frontend/src/components/ServerForm.tsx index f44dc44..99b8a9d 100644 --- a/frontend/src/components/ServerForm.tsx +++ b/frontend/src/components/ServerForm.tsx @@ -429,7 +429,6 @@ const ServerForm = ({ className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input" placeholder="e.g.: time-mcp" required - disabled={isEdit} /> diff --git a/mcp_settings.json b/mcp_settings.json index 9a0ad00..9a2bcbd 100644 --- a/mcp_settings.json +++ b/mcp_settings.json @@ -63,5 +63,6 @@ "requiresAuthentication": false } } - } + }, + "bearerKeys": [] } \ No newline at end of file diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 5433dd4..50ddf88 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -423,7 +423,7 @@ export const deleteServer = async (req: Request, res: Response): Promise = export const updateServer = async (req: Request, res: Response): Promise => { try { const { name } = req.params; - const { config } = req.body; + const { config, newName } = req.body; if (!name) { res.status(400).json({ success: false, @@ -510,12 +510,52 @@ export const updateServer = async (req: Request, res: Response): Promise = config.owner = currentUser?.username || 'admin'; } - const result = await addOrUpdateServer(name, config, true); // Allow override for updates + // Check if server name is being changed + const isRenaming = newName && newName !== name; + + // If renaming, validate the new name and update references + if (isRenaming) { + const serverDao = getServerDao(); + + // Check if new name already exists + if (await serverDao.exists(newName)) { + res.status(400).json({ + success: false, + message: `Server name '${newName}' already exists`, + }); + return; + } + + // Rename the server + const renamed = await serverDao.rename(name, newName); + if (!renamed) { + res.status(404).json({ + success: false, + message: 'Server not found', + }); + return; + } + + // Update references in groups + const groupDao = getGroupDao(); + await groupDao.updateServerName(name, newName); + + // Update references in bearer keys + const bearerKeyDao = getBearerKeyDao(); + await bearerKeyDao.updateServerName(name, newName); + } + + // Use the final server name (new name if renaming, otherwise original name) + const finalName = isRenaming ? newName : name; + + const result = await addOrUpdateServer(finalName, config, true); // Allow override for updates if (result.success) { - notifyToolChanged(name); + notifyToolChanged(finalName); res.json({ success: true, - message: 'Server updated successfully', + message: isRenaming + ? `Server renamed and updated successfully` + : 'Server updated successfully', }); } else { res.status(404).json({ @@ -524,9 +564,10 @@ export const updateServer = async (req: Request, res: Response): Promise = }); } } catch (error) { + console.error('Failed to update server:', error); res.status(500).json({ success: false, - message: 'Internal server error', + message: error instanceof Error ? error.message : 'Internal server error', }); } }; diff --git a/src/dao/BearerKeyDao.ts b/src/dao/BearerKeyDao.ts index 812883b..02b2d4f 100644 --- a/src/dao/BearerKeyDao.ts +++ b/src/dao/BearerKeyDao.ts @@ -13,6 +13,10 @@ export interface BearerKeyDao { create(data: Omit): Promise; update(id: string, data: Partial>): Promise; delete(id: string): Promise; + /** + * Update server name in all bearer keys (when server is renamed) + */ + updateServerName(oldName: string, newName: string): Promise; } /** @@ -122,4 +126,34 @@ export class BearerKeyDaoImpl extends JsonFileBaseDao implements BearerKeyDao { await this.saveKeys(next); return true; } + + async updateServerName(oldName: string, newName: string): Promise { + const keys = await this.loadKeysWithMigration(); + let updatedCount = 0; + + for (const key of keys) { + let updated = false; + + if (key.allowedServers && key.allowedServers.length > 0) { + const newServers = key.allowedServers.map((server) => { + if (server === oldName) { + updated = true; + return newName; + } + return server; + }); + + if (updated) { + key.allowedServers = newServers; + updatedCount++; + } + } + } + + if (updatedCount > 0) { + await this.saveKeys(keys); + } + + return updatedCount; + } } diff --git a/src/dao/BearerKeyDaoDbImpl.ts b/src/dao/BearerKeyDaoDbImpl.ts index 80a7448..c17bef0 100644 --- a/src/dao/BearerKeyDaoDbImpl.ts +++ b/src/dao/BearerKeyDaoDbImpl.ts @@ -74,4 +74,30 @@ export class BearerKeyDaoDbImpl implements BearerKeyDao { async delete(id: string): Promise { return await this.repository.delete(id); } + + async updateServerName(oldName: string, newName: string): Promise { + const allKeys = await this.repository.findAll(); + let updatedCount = 0; + + for (const key of allKeys) { + let updated = false; + + if (key.allowedServers && key.allowedServers.length > 0) { + const newServers = key.allowedServers.map((server) => { + if (server === oldName) { + updated = true; + return newName; + } + return server; + }); + + if (updated) { + await this.repository.update(key.id, { allowedServers: newServers }); + updatedCount++; + } + } + } + + return updatedCount; + } } diff --git a/src/dao/GroupDao.ts b/src/dao/GroupDao.ts index b8f6447..8ceaa7f 100644 --- a/src/dao/GroupDao.ts +++ b/src/dao/GroupDao.ts @@ -36,6 +36,11 @@ export interface GroupDao extends BaseDao { * Find group by name */ findByName(name: string): Promise; + + /** + * Update server name in all groups (when server is renamed) + */ + updateServerName(oldName: string, newName: string): Promise; } /** @@ -218,4 +223,39 @@ export class GroupDaoImpl extends JsonFileBaseDao implements GroupDao { const groups = await this.getAll(); return groups.find((group) => group.name === name) || null; } + + async updateServerName(oldName: string, newName: string): Promise { + const groups = await this.getAll(); + let updatedCount = 0; + + for (const group of groups) { + let updated = false; + const newServers = group.servers.map((server) => { + if (typeof server === 'string') { + if (server === oldName) { + updated = true; + return newName; + } + return server; + } else { + if (server.name === oldName) { + updated = true; + return { ...server, name: newName }; + } + return server; + } + }) as IGroup['servers']; + + if (updated) { + group.servers = newServers; + updatedCount++; + } + } + + if (updatedCount > 0) { + await this.saveAll(groups); + } + + return updatedCount; + } } diff --git a/src/dao/GroupDaoDbImpl.ts b/src/dao/GroupDaoDbImpl.ts index 80b355c..bbdfa37 100644 --- a/src/dao/GroupDaoDbImpl.ts +++ b/src/dao/GroupDaoDbImpl.ts @@ -151,4 +151,35 @@ export class GroupDaoDbImpl implements GroupDao { owner: group.owner, }; } + + async updateServerName(oldName: string, newName: string): Promise { + const allGroups = await this.repository.findAll(); + let updatedCount = 0; + + for (const group of allGroups) { + let updated = false; + const newServers = group.servers.map((server) => { + if (typeof server === 'string') { + if (server === oldName) { + updated = true; + return newName; + } + return server; + } else { + if (server.name === oldName) { + updated = true; + return { ...server, name: newName }; + } + return server; + } + }); + + if (updated) { + await this.update(group.id, { servers: newServers as any }); + updatedCount++; + } + } + + return updatedCount; + } } diff --git a/src/dao/ServerDao.ts b/src/dao/ServerDao.ts index 38f1615..50eb102 100644 --- a/src/dao/ServerDao.ts +++ b/src/dao/ServerDao.ts @@ -41,6 +41,11 @@ export interface ServerDao extends BaseDao { name: string, prompts: Record, ): Promise; + + /** + * Rename a server (change its name/key) + */ + rename(oldName: string, newName: string): Promise; } /** @@ -95,7 +100,8 @@ export class ServerDaoImpl extends JsonFileBaseDao implements ServerDao { return { ...existing, ...updates, - name: existing.name, // Name should not be updated + // Keep the existing name unless explicitly updating via rename + name: updates.name ?? existing.name, }; } @@ -141,9 +147,7 @@ export class ServerDaoImpl extends JsonFileBaseDao implements ServerDao { return null; } - // Don't allow name changes - const { name: _, ...allowedUpdates } = updates; - const updatedServer = this.updateEntity(servers[index], allowedUpdates); + const updatedServer = this.updateEntity(servers[index], updates); servers[index] = updatedServer; await this.saveAll(servers); @@ -207,4 +211,22 @@ export class ServerDaoImpl extends JsonFileBaseDao implements ServerDao { const result = await this.update(name, { prompts }); return result !== null; } + + async rename(oldName: string, newName: string): Promise { + const servers = await this.getAll(); + const index = servers.findIndex((server) => server.name === oldName); + + if (index === -1) { + return false; + } + + // Check if newName already exists + if (servers.find((server) => server.name === newName)) { + throw new Error(`Server ${newName} already exists`); + } + + servers[index] = { ...servers[index], name: newName }; + await this.saveAll(servers); + return true; + } } diff --git a/src/dao/ServerDaoDbImpl.ts b/src/dao/ServerDaoDbImpl.ts index 6ba9a74..12ce3d8 100644 --- a/src/dao/ServerDaoDbImpl.ts +++ b/src/dao/ServerDaoDbImpl.ts @@ -115,6 +115,15 @@ export class ServerDaoDbImpl implements ServerDao { return result !== null; } + async rename(oldName: string, newName: string): Promise { + // Check if newName already exists + if (await this.repository.exists(newName)) { + throw new Error(`Server ${newName} already exists`); + } + + return await this.repository.rename(oldName, newName); + } + private mapToServerConfig(server: { name: string; type?: string; diff --git a/src/db/repositories/ServerRepository.ts b/src/db/repositories/ServerRepository.ts index 56efdf4..48c6bdd 100644 --- a/src/db/repositories/ServerRepository.ts +++ b/src/db/repositories/ServerRepository.ts @@ -89,6 +89,19 @@ export class ServerRepository { async setEnabled(name: string, enabled: boolean): Promise { return await this.update(name, { enabled }); } + + /** + * Rename a server + */ + async rename(oldName: string, newName: string): Promise { + const server = await this.findByName(oldName); + if (!server) { + return false; + } + server.name = newName; + await this.repository.save(server); + return true; + } } export default ServerRepository;