@railgun52/dsh-mcp-servers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/README.zh.md +64 -0
  4. package/cordis.patch.yml +3 -0
  5. package/icon.svg +5 -0
  6. package/lib/client.js +357 -0
  7. package/lib/client.js.map +1 -0
  8. package/lib/index.js +150 -0
  9. package/lib/invariant.js +8 -0
  10. package/lib/types/client/McpSection.d.ts +9 -0
  11. package/lib/types/client/McpSection.d.ts.map +1 -0
  12. package/lib/types/client/McpSection.js +140 -0
  13. package/lib/types/client/McpSection.js.map +1 -0
  14. package/lib/types/client/index.d.ts +10 -0
  15. package/lib/types/client/index.d.ts.map +1 -0
  16. package/lib/types/client/index.js +21 -0
  17. package/lib/types/client/index.js.map +1 -0
  18. package/lib/types/client/locales.d.ts +22 -0
  19. package/lib/types/client/locales.d.ts.map +1 -0
  20. package/lib/types/client/locales.js +35 -0
  21. package/lib/types/client/locales.js.map +1 -0
  22. package/lib/types/index.d.ts +59 -0
  23. package/lib/types/index.d.ts.map +1 -0
  24. package/lib/types/index.js +36 -0
  25. package/lib/types/index.js.map +1 -0
  26. package/lib/types/invariant.d.ts +5 -0
  27. package/lib/types/invariant.d.ts.map +1 -0
  28. package/lib/types/invariant.js +6 -0
  29. package/lib/types/invariant.js.map +1 -0
  30. package/lib/types/mcp-bridge.d.ts +20 -0
  31. package/lib/types/mcp-bridge.d.ts.map +1 -0
  32. package/lib/types/mcp-bridge.js +100 -0
  33. package/lib/types/mcp-bridge.js.map +1 -0
  34. package/lib/types/mcp-servers-schema.d.ts +66 -0
  35. package/lib/types/mcp-servers-schema.d.ts.map +1 -0
  36. package/lib/types/mcp-servers-schema.js +22 -0
  37. package/lib/types/mcp-servers-schema.js.map +1 -0
  38. package/package.json +96 -0
  39. package/src/client/McpSection.module.css +143 -0
  40. package/src/client/McpSection.tsx +248 -0
  41. package/src/client/index.ts +38 -0
  42. package/src/client/locales.ts +35 -0
  43. package/src/css-modules.d.ts +4 -0
  44. package/src/index.ts +45 -0
  45. package/src/invariant.ts +8 -0
  46. package/src/mcp-bridge.ts +111 -0
  47. package/src/mcp-servers-schema.ts +45 -0
@@ -0,0 +1,38 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
2
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
3
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
4
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
5
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
6
+ import { McpSection } from './McpSection.tsx'
7
+ import type { McpSectionInjected } from './McpSection.tsx'
8
+ import { en, zh, type McpKey } from './locales.ts'
9
+
10
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
11
+ interface LocaleNamespaceMap {
12
+ 'settings.mcp': McpKey
13
+ }
14
+ }
15
+ const NS = 'settings.mcp'
16
+ const NAMESPACE = 'mcp-servers'
17
+
18
+ interface McpServersConfig {
19
+ servers: unknown[]
20
+ }
21
+
22
+ export const inject = ['slots', 'locale', 'configForms']
23
+
24
+ export function apply(ctx: ClientContext): void {
25
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-mcp: copy dictionaries')
26
+ const form = ctx.configForms.get<McpServersConfig>(NAMESPACE)
27
+ const t = ctx.locale.bind(NS) as McpSectionInjected['t']
28
+ const load = (): unknown[] => form.getSnapshot().value?.servers ?? []
29
+ const save = async (servers: unknown[]): Promise<void> => { await form.set('servers', servers) }
30
+ ctx.effect(() => ctx.configForms.whileServed([NAMESPACE], () => ctx.slots.inject('settings.section', () => ctx.slots.register({
31
+ name: 'settings.section',
32
+ id: 'mcp',
33
+ order: 25,
34
+ label: () => t('nav'),
35
+ locale: NS,
36
+ inject: (): McpSectionInjected => ({ load, save, t }),
37
+ }, McpSection))), 'ui-settings-mcp: page')
38
+ }
@@ -0,0 +1,35 @@
1
+ export const en = {
2
+ nav: 'MCP Servers',
3
+ title: 'MCP Configuration',
4
+ intro: 'Manage your MCP servers by editing the JSON configuration below.',
5
+ add: 'Add server',
6
+ edit: 'Edit MCP config',
7
+ remove: 'Delete',
8
+ deleteConfirm: 'Delete {server}?',
9
+ deleteDescription: 'This will remove the MCP server configuration.',
10
+ save: 'Save',
11
+ cancel: 'Cancel',
12
+ close: 'Close',
13
+ noServers: 'No MCP servers configured. Click "Add server" to get started.',
14
+ loadFailed: 'Failed to load MCP configuration.',
15
+ saveFailed: 'Failed to save MCP configuration.',
16
+ saved: 'Configuration saved.',
17
+ }
18
+ export type McpKey = keyof typeof en
19
+ export const zh: { [Key in keyof typeof en]: string } = {
20
+ nav: 'MCP 服务器',
21
+ title: 'MCP 配置',
22
+ intro: '通过编辑下方的 JSON 配置来管理 MCP 服务器。',
23
+ add: '添加服务器',
24
+ edit: '编辑 MCP 配置',
25
+ remove: '删除',
26
+ deleteConfirm: '删除 {server}?',
27
+ deleteDescription: '这将移除 MCP 服务器配置。',
28
+ save: '保存',
29
+ cancel: '取消',
30
+ close: '关闭',
31
+ noServers: '还没有配置 MCP 服务器,点击"添加服务器"开始配置。',
32
+ loadFailed: '加载 MCP 配置失败。',
33
+ saveFailed: '保存 MCP 配置失败。',
34
+ saved: '配置已保存。',
35
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.module.css' {
2
+ const classes: Record<string, string>
3
+ export default classes
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Host registration: the `mcp-servers` settings namespace and the live
3
+ * mcp-client bridge it feeds. Under the profile-config settings model the
4
+ * plugin's Config schema IS the namespace, so the owning profile entry id must
5
+ * equal `mcp-servers`. `servers` is volatile, so edits reload the value without
6
+ * remounting this plugin; the bridge re-syncs on `loader/volatile-update`.
7
+ */
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+ import type {} from '@deepseek-ai/cordis-plugin-loader'
11
+ import type {} from '@deepseek-ai/dsh-tools'
12
+ import { McpServersSchema } from './mcp-servers-schema.ts'
13
+ import type { McpServersHostConfig } from './mcp-servers-schema.ts'
14
+ import { applyMcpBridge } from './mcp-bridge.ts'
15
+
16
+ export { MCP_SERVERS_NAMESPACE, McpServersSchema } from './mcp-servers-schema.ts'
17
+ export type { McpServerConfig, McpServersSettings } from './mcp-servers-schema.ts'
18
+
19
+ /** Services required: the mcp-client instances this plugin mounts inject the tool registry. */
20
+ export const inject = ['tools']
21
+
22
+ /** Config schema; the owning profile entry's id must equal `mcp-servers`. */
23
+ export const Config = McpServersSchema
24
+
25
+ /**
26
+ * Keep live mcp-client instances in sync with the resolved `mcp-servers`
27
+ * config, so the Settings page's MCP edits take effect without a restart.
28
+ * @param ctx - Host context whose tool registry the mounted mcp-client instances need.
29
+ * @param config - resolved `mcp-servers` section; `servers` is a live volatile reference.
30
+ */
31
+ export function apply(ctx: Context, config: McpServersHostConfig): void {
32
+ const bridge = applyMcpBridge(ctx, () => ({
33
+ servers: config.servers.get().map(server => ({
34
+ ...server,
35
+ args: [...server.args],
36
+ env: { ...server.env },
37
+ })),
38
+ }))
39
+ ctx.on('loader/volatile-update', () => {
40
+ bridge.sync()
41
+ })
42
+ ctx.effect(() => () => {
43
+ void bridge.dispose()
44
+ }, 'mcp-servers: settings bridge')
45
+ }
@@ -0,0 +1,8 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
3
+ const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-settings-mcp'
4
+ export const name = 'client-ui-settings-mcp-invariant'
5
+ export const inject = ['invariants']
6
+ const install: InvariantInstaller = () => {}
7
+ export const apply = (ctx: Context): Promise<() => void> =>
8
+ Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
@@ -0,0 +1,111 @@
1
+ /** Settings -> mcp-client bridge: live-mounts MCP servers from the settings document. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import * as McpClient from '@deepseek-ai/dsh-mcp-client'
5
+ import type { McpServerConfig } from './mcp-servers-schema.ts'
6
+
7
+ type PluginFiber = ReturnType<Context['plugin']>
8
+
9
+ interface LiveInstance {
10
+ fiber: PluginFiber
11
+ signature: string
12
+ }
13
+
14
+ /**
15
+ * Build one mcp-client config from a settings entry. The settings service
16
+ * deeply freezes published values, while Schemastery's config parse fills
17
+ * defaults into the args array and env dict, so both are cloned first.
18
+ * @param server - one entry of the mcp-servers settings value.
19
+ * @returns validated mcp-client config for that server.
20
+ */
21
+ function toClientConfig(server: McpServerConfig): McpClient.Config {
22
+ if (server.transport === 'streamable-http') {
23
+ return McpClient.Config({
24
+ transport: 'streamable-http',
25
+ serverName: server.name,
26
+ url: server.url,
27
+ headers: {},
28
+ failOnStartupError: false,
29
+ })
30
+ }
31
+ return McpClient.Config({
32
+ transport: 'stdio',
33
+ serverName: server.name,
34
+ command: server.command,
35
+ args: [...server.args],
36
+ env: { ...server.env },
37
+ cwd: '',
38
+ failOnStartupError: false,
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Mount and keep live mcp-client instances in sync with the `mcp-servers` settings
44
+ * namespace. Creates, rebuilds, and disposes instances by `serverName` so editing the
45
+ * settings page takes effect without a restart. All sync work is serialized on one
46
+ * promise chain so concurrent settings writes cannot race the instance map.
47
+ * @param ctx - Host context carrying the tool registry required by mcp-client.
48
+ * @param getValue - accessor for the resolved settings value (called at sync time).
49
+ * @returns a sync trigger (call on settings change) and a dispose that drains the chain
50
+ * and tears down every live instance.
51
+ */
52
+ export function applyMcpBridge(
53
+ ctx: Context,
54
+ getValue: () => { servers: McpServerConfig[] } | undefined,
55
+ ): { sync: () => void; dispose: () => Promise<void> } {
56
+ const instances = new Map<string, LiveInstance>()
57
+ let chain: Promise<void> = Promise.resolve()
58
+
59
+ const syncOnce = async (): Promise<void> => {
60
+ const servers = getValue()?.servers ?? []
61
+ const next = new Map<string, McpServerConfig>()
62
+ for (const server of servers) {
63
+ if (!server.enabled) continue
64
+ if (next.has(server.name)) continue
65
+ next.set(server.name, server)
66
+ }
67
+ for (const [name, server] of next) {
68
+ const signature = JSON.stringify(server)
69
+ const existing = instances.get(name)
70
+ if (existing !== undefined && existing.signature === signature) continue
71
+ if (existing !== undefined) {
72
+ await existing.fiber.dispose()
73
+ instances.delete(name)
74
+ }
75
+ let config: McpClient.Config
76
+ try {
77
+ config = toClientConfig(server)
78
+ } catch (error: unknown) {
79
+ ctx.logger.warn('mcp-bridge: invalid server config for "%s": %s', name, String(error))
80
+ continue
81
+ }
82
+ const fiber = ctx.plugin(McpClient, config)
83
+ instances.set(name, { fiber, signature })
84
+ }
85
+ for (const [name, { fiber }] of instances) {
86
+ if (!next.has(name)) {
87
+ await fiber.dispose()
88
+ instances.delete(name)
89
+ }
90
+ }
91
+ }
92
+
93
+ const sync = (): void => {
94
+ chain = chain.then(syncOnce).catch((error: unknown) => {
95
+ ctx.logger.warn('mcp-bridge: sync failed: %s', String(error))
96
+ })
97
+ }
98
+
99
+ const dispose = async (): Promise<void> => {
100
+ chain = chain.finally(async () => {
101
+ for (const [, { fiber }] of instances) {
102
+ try { await fiber.dispose() } catch { /* instance already gone */ }
103
+ }
104
+ instances.clear()
105
+ })
106
+ await chain
107
+ }
108
+
109
+ sync()
110
+ return { sync, dispose }
111
+ }
@@ -0,0 +1,45 @@
1
+ /** MCP servers settings schema (host + browser shared). */
2
+
3
+ import type { Volatile } from '@deepseek-ai/cordis'
4
+ import z from '@deepseek-ai/schemastery'
5
+
6
+ /** Settings namespace owned by the MCP servers settings bridge. */
7
+ export const MCP_SERVERS_NAMESPACE = 'mcp-servers'
8
+
9
+ /** One MCP server declaration as stored in the user-settings document. */
10
+ export interface McpServerConfig {
11
+ name: string
12
+ command: string
13
+ args: string[]
14
+ env: Record<string, string>
15
+ transport: 'stdio' | 'streamable-http'
16
+ url: string
17
+ enabled: boolean
18
+ }
19
+
20
+ /** Durable MCP servers section shared by the Host schema and the browser scope. */
21
+ export interface McpServersSettings {
22
+ servers: McpServerConfig[]
23
+ }
24
+
25
+ const McpServerSchema = z.object({
26
+ name: z.string().required(),
27
+ command: z.string(),
28
+ args: z.array(String).default([]),
29
+ env: z.dict(String).default({}),
30
+ transport: z.union(['stdio', 'streamable-http']).default('stdio'),
31
+ url: z.string().default(''),
32
+ enabled: z.boolean().default(true),
33
+ })
34
+
35
+ /**
36
+ * Durable MCP servers schema. `servers` is volatile so the profile-config
37
+ * settings model exposes it as a live-editable field (the plugin is not
38
+ * remounted on edit; the bridge re-syncs on `loader/volatile-update`).
39
+ */
40
+ export const McpServersSchema = z.object({
41
+ servers: z.array(McpServerSchema).default([]).volatile(),
42
+ })
43
+
44
+ /** Resolved Host config; the volatile field is read through its live reference. */
45
+ export type McpServersHostConfig = { servers: Volatile<McpServerConfig[]> }