mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Desktop MCP settings was synchronously probing every configured server from /api/mcp, which made the list page scale with connection latency and surface brittle behavior as installs accumulated more MCPs. Return lightweight snapshot rows from the list API, keep explicit status checks separate, and let the desktop UI refresh status in a constrained background lane while preserving project-aware server identity. Constraint: MCP list must stay responsive even with many configured servers Rejected: Probe all servers from the list view without limits | still fans out with server count and can overload slow installs Rejected: Keep servers permanently unchecked until detail view | misses the desired loading feedback when the MCP page opens Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep /api/mcp as a configuration snapshot endpoint; do not reintroduce per-row live connect work on list load Tested: bun test src/server/__tests__/mcp.test.ts; cd desktop && bun run test -- mcpSettings.test.tsx; cd desktop && bun x tsc --noEmit --ignoreDeprecations 5.0 Not-tested: Real desktop interaction against an environment with dozens of live MCP servers
170 lines
6.1 KiB
TypeScript
170 lines
6.1 KiB
TypeScript
import { create } from 'zustand'
|
|
import { mcpApi } from '../api/mcp'
|
|
import type { McpServerRecord, McpUpsertPayload } from '../types/mcp'
|
|
|
|
type McpStore = {
|
|
servers: McpServerRecord[]
|
|
selectedServer: McpServerRecord | null
|
|
isLoading: boolean
|
|
error: string | null
|
|
fetchServers: (projectPaths?: string[], fallbackCwd?: string) => Promise<void>
|
|
createServer: (name: string, payload: McpUpsertPayload, cwd?: string) => Promise<McpServerRecord>
|
|
updateServer: (server: McpServerRecord, payload: McpUpsertPayload, cwd?: string) => Promise<McpServerRecord>
|
|
deleteServer: (server: McpServerRecord, cwd?: string) => Promise<void>
|
|
toggleServer: (server: McpServerRecord, cwd?: string) => Promise<McpServerRecord>
|
|
reconnectServer: (server: McpServerRecord, cwd?: string) => Promise<McpServerRecord>
|
|
refreshServerStatus: (server: McpServerRecord, cwd?: string) => Promise<McpServerRecord>
|
|
selectServer: (server: McpServerRecord | null) => void
|
|
}
|
|
|
|
function isProjectScoped(server: Pick<McpServerRecord, 'scope'>) {
|
|
return server.scope === 'local' || server.scope === 'project'
|
|
}
|
|
|
|
function attachProjectPath(server: McpServerRecord, cwd?: string) {
|
|
if (!isProjectScoped(server)) {
|
|
return {
|
|
...server,
|
|
projectPath: undefined,
|
|
}
|
|
}
|
|
|
|
return {
|
|
...server,
|
|
projectPath: cwd ?? server.projectPath,
|
|
}
|
|
}
|
|
|
|
function isSameServer(a: Pick<McpServerRecord, 'name' | 'scope' | 'projectPath'>, b: Pick<McpServerRecord, 'name' | 'scope' | 'projectPath'>) {
|
|
if (a.name !== b.name || a.scope !== b.scope) return false
|
|
if (!isProjectScoped(a) && !isProjectScoped(b)) return true
|
|
return (a.projectPath ?? '') === (b.projectPath ?? '')
|
|
}
|
|
|
|
function replaceServer(
|
|
servers: McpServerRecord[],
|
|
previous: Pick<McpServerRecord, 'name' | 'scope' | 'projectPath'>,
|
|
next: McpServerRecord,
|
|
cwd?: string,
|
|
) {
|
|
const normalizedNext = attachProjectPath(next, cwd)
|
|
const index = servers.findIndex((item) => isSameServer(item, previous))
|
|
if (index === -1) return [...servers, normalizedNext]
|
|
|
|
return servers.map((item, itemIndex) => (itemIndex === index ? normalizedNext : item))
|
|
}
|
|
|
|
export const useMcpStore = create<McpStore>((set) => ({
|
|
servers: [],
|
|
selectedServer: null,
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
fetchServers: async (projectPaths, fallbackCwd) => {
|
|
set({ isLoading: true, error: null })
|
|
try {
|
|
const normalizedPaths = Array.from(new Set((projectPaths ?? []).filter(Boolean)))
|
|
const contexts = normalizedPaths.length > 0 ? normalizedPaths : [fallbackCwd].filter(Boolean)
|
|
|
|
const responses = await Promise.all(
|
|
(contexts.length > 0 ? contexts : [undefined]).map(async (cwd) => {
|
|
const response = await mcpApi.list(cwd)
|
|
return response.servers.map((server) => ({
|
|
...server,
|
|
projectPath: server.scope === 'local' || server.scope === 'project' ? cwd : undefined,
|
|
}))
|
|
}),
|
|
)
|
|
|
|
const deduped = new Map<string, McpServerRecord>()
|
|
for (const group of responses) {
|
|
for (const server of group) {
|
|
const key =
|
|
server.scope === 'local' || server.scope === 'project'
|
|
? `${server.scope}:${server.projectPath}:${server.name}`
|
|
: `${server.scope}:${server.name}`
|
|
if (!deduped.has(key)) {
|
|
deduped.set(key, server)
|
|
}
|
|
}
|
|
}
|
|
|
|
set({ servers: [...deduped.values()], isLoading: false })
|
|
} catch (error) {
|
|
set({
|
|
isLoading: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load MCP servers',
|
|
})
|
|
}
|
|
},
|
|
|
|
createServer: async (name, payload, cwd) => {
|
|
const response = await mcpApi.create(name, payload, cwd)
|
|
const created = attachProjectPath(response.server, cwd)
|
|
set((state) => ({
|
|
servers: [...state.servers, created],
|
|
selectedServer: created,
|
|
error: null,
|
|
}))
|
|
return created
|
|
},
|
|
|
|
updateServer: async (server, payload, cwd) => {
|
|
const response = await mcpApi.update(server.name, payload, cwd)
|
|
const updated = attachProjectPath(response.server, cwd ?? server.projectPath)
|
|
set((state) => ({
|
|
servers: replaceServer(state.servers, server, updated, cwd ?? server.projectPath),
|
|
selectedServer: state.selectedServer && isSameServer(state.selectedServer, server) ? updated : state.selectedServer,
|
|
error: null,
|
|
}))
|
|
return updated
|
|
},
|
|
|
|
deleteServer: async (server, cwd) => {
|
|
await mcpApi.remove(server.name, server.scope, cwd)
|
|
set((state) => ({
|
|
servers: state.servers.filter((item) => !isSameServer(item, server)),
|
|
selectedServer:
|
|
state.selectedServer && isSameServer(state.selectedServer, server)
|
|
? null
|
|
: state.selectedServer,
|
|
error: null,
|
|
}))
|
|
},
|
|
|
|
toggleServer: async (server, cwd) => {
|
|
const response = await mcpApi.toggle(server.name, cwd)
|
|
const updated = attachProjectPath(response.server, cwd ?? server.projectPath)
|
|
set((state) => ({
|
|
servers: replaceServer(state.servers, server, updated, cwd ?? server.projectPath),
|
|
selectedServer: state.selectedServer && isSameServer(state.selectedServer, server) ? updated : state.selectedServer,
|
|
error: null,
|
|
}))
|
|
return updated
|
|
},
|
|
|
|
reconnectServer: async (server, cwd) => {
|
|
const response = await mcpApi.reconnect(server.name, cwd)
|
|
const updated = attachProjectPath(response.server, cwd ?? server.projectPath)
|
|
set((state) => ({
|
|
servers: replaceServer(state.servers, server, updated, cwd ?? server.projectPath),
|
|
selectedServer: state.selectedServer && isSameServer(state.selectedServer, server) ? updated : state.selectedServer,
|
|
error: null,
|
|
}))
|
|
return updated
|
|
},
|
|
|
|
refreshServerStatus: async (server, cwd) => {
|
|
const response = await mcpApi.status(server.name, cwd)
|
|
const updated = attachProjectPath(response.server, cwd ?? server.projectPath)
|
|
set((state) => ({
|
|
servers: replaceServer(state.servers, server, updated, cwd ?? server.projectPath),
|
|
selectedServer: state.selectedServer && isSameServer(state.selectedServer, server) ? updated : state.selectedServer,
|
|
error: null,
|
|
}))
|
|
return updated
|
|
},
|
|
|
|
selectServer: (server) => set({ selectedServer: server }),
|
|
}))
|