@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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/README.zh.md +64 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +5 -0
- package/lib/client.js +357 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +150 -0
- package/lib/invariant.js +8 -0
- package/lib/types/client/McpSection.d.ts +9 -0
- package/lib/types/client/McpSection.d.ts.map +1 -0
- package/lib/types/client/McpSection.js +140 -0
- package/lib/types/client/McpSection.js.map +1 -0
- package/lib/types/client/index.d.ts +10 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +21 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/locales.d.ts +22 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +35 -0
- package/lib/types/client/locales.js.map +1 -0
- package/lib/types/index.d.ts +59 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +36 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/invariant.d.ts +5 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +6 -0
- package/lib/types/invariant.js.map +1 -0
- package/lib/types/mcp-bridge.d.ts +20 -0
- package/lib/types/mcp-bridge.d.ts.map +1 -0
- package/lib/types/mcp-bridge.js +100 -0
- package/lib/types/mcp-bridge.js.map +1 -0
- package/lib/types/mcp-servers-schema.d.ts +66 -0
- package/lib/types/mcp-servers-schema.d.ts.map +1 -0
- package/lib/types/mcp-servers-schema.js +22 -0
- package/lib/types/mcp-servers-schema.js.map +1 -0
- package/package.json +96 -0
- package/src/client/McpSection.module.css +143 -0
- package/src/client/McpSection.tsx +248 -0
- package/src/client/index.ts +38 -0
- package/src/client/locales.ts +35 -0
- package/src/css-modules.d.ts +4 -0
- package/src/index.ts +45 -0
- package/src/invariant.ts +8 -0
- package/src/mcp-bridge.ts +111 -0
- package/src/mcp-servers-schema.ts +45 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** Settings -> mcp-client bridge: live-mounts MCP servers from the settings document. */
|
|
2
|
+
import * as McpClient from '@deepseek-ai/dsh-mcp-client';
|
|
3
|
+
/**
|
|
4
|
+
* Build one mcp-client config from a settings entry. The settings service
|
|
5
|
+
* deeply freezes published values, while Schemastery's config parse fills
|
|
6
|
+
* defaults into the args array and env dict, so both are cloned first.
|
|
7
|
+
* @param server - one entry of the mcp-servers settings value.
|
|
8
|
+
* @returns validated mcp-client config for that server.
|
|
9
|
+
*/
|
|
10
|
+
function toClientConfig(server) {
|
|
11
|
+
if (server.transport === 'streamable-http') {
|
|
12
|
+
return McpClient.Config({
|
|
13
|
+
transport: 'streamable-http',
|
|
14
|
+
serverName: server.name,
|
|
15
|
+
url: server.url,
|
|
16
|
+
headers: {},
|
|
17
|
+
failOnStartupError: false,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return McpClient.Config({
|
|
21
|
+
transport: 'stdio',
|
|
22
|
+
serverName: server.name,
|
|
23
|
+
command: server.command,
|
|
24
|
+
args: [...server.args],
|
|
25
|
+
env: { ...server.env },
|
|
26
|
+
cwd: '',
|
|
27
|
+
failOnStartupError: false,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Mount and keep live mcp-client instances in sync with the `mcp-servers` settings
|
|
32
|
+
* namespace. Creates, rebuilds, and disposes instances by `serverName` so editing the
|
|
33
|
+
* settings page takes effect without a restart. All sync work is serialized on one
|
|
34
|
+
* promise chain so concurrent settings writes cannot race the instance map.
|
|
35
|
+
* @param ctx - Host context carrying the tool registry required by mcp-client.
|
|
36
|
+
* @param getValue - accessor for the resolved settings value (called at sync time).
|
|
37
|
+
* @returns a sync trigger (call on settings change) and a dispose that drains the chain
|
|
38
|
+
* and tears down every live instance.
|
|
39
|
+
*/
|
|
40
|
+
export function applyMcpBridge(ctx, getValue) {
|
|
41
|
+
const instances = new Map();
|
|
42
|
+
let chain = Promise.resolve();
|
|
43
|
+
const syncOnce = async () => {
|
|
44
|
+
const servers = getValue()?.servers ?? [];
|
|
45
|
+
const next = new Map();
|
|
46
|
+
for (const server of servers) {
|
|
47
|
+
if (!server.enabled)
|
|
48
|
+
continue;
|
|
49
|
+
if (next.has(server.name))
|
|
50
|
+
continue;
|
|
51
|
+
next.set(server.name, server);
|
|
52
|
+
}
|
|
53
|
+
for (const [name, server] of next) {
|
|
54
|
+
const signature = JSON.stringify(server);
|
|
55
|
+
const existing = instances.get(name);
|
|
56
|
+
if (existing !== undefined && existing.signature === signature)
|
|
57
|
+
continue;
|
|
58
|
+
if (existing !== undefined) {
|
|
59
|
+
await existing.fiber.dispose();
|
|
60
|
+
instances.delete(name);
|
|
61
|
+
}
|
|
62
|
+
let config;
|
|
63
|
+
try {
|
|
64
|
+
config = toClientConfig(server);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
ctx.logger.warn('mcp-bridge: invalid server config for "%s": %s', name, String(error));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const fiber = ctx.plugin(McpClient, config);
|
|
71
|
+
instances.set(name, { fiber, signature });
|
|
72
|
+
}
|
|
73
|
+
for (const [name, { fiber }] of instances) {
|
|
74
|
+
if (!next.has(name)) {
|
|
75
|
+
await fiber.dispose();
|
|
76
|
+
instances.delete(name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const sync = () => {
|
|
81
|
+
chain = chain.then(syncOnce).catch((error) => {
|
|
82
|
+
ctx.logger.warn('mcp-bridge: sync failed: %s', String(error));
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const dispose = async () => {
|
|
86
|
+
chain = chain.finally(async () => {
|
|
87
|
+
for (const [, { fiber }] of instances) {
|
|
88
|
+
try {
|
|
89
|
+
await fiber.dispose();
|
|
90
|
+
}
|
|
91
|
+
catch { /* instance already gone */ }
|
|
92
|
+
}
|
|
93
|
+
instances.clear();
|
|
94
|
+
});
|
|
95
|
+
await chain;
|
|
96
|
+
};
|
|
97
|
+
sync();
|
|
98
|
+
return { sync, dispose };
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=mcp-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-bridge.js","sourceRoot":"","sources":["../../src/mcp-bridge.ts"],"names":[],"mappings":"AAAA,yFAAyF;AAGzF,OAAO,KAAK,SAAS,MAAM,6BAA6B,CAAA;AAUxD;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,MAAuB;IAC7C,IAAI,MAAM,CAAC,SAAS,KAAK,iBAAiB,EAAE,CAAC;QAC3C,OAAO,SAAS,CAAC,MAAM,CAAC;YACtB,SAAS,EAAE,iBAAiB;YAC5B,UAAU,EAAE,MAAM,CAAC,IAAI;YACvB,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,OAAO,EAAE,EAAE;YACX,kBAAkB,EAAE,KAAK;SAC1B,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,SAAS,CAAC,MAAM,CAAC;QACtB,SAAS,EAAE,OAAO;QAClB,UAAU,EAAE,MAAM,CAAC,IAAI;QACvB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;QACtB,GAAG,EAAE,EAAE,GAAG,MAAM,CAAC,GAAG,EAAE;QACtB,GAAG,EAAE,EAAE;QACP,kBAAkB,EAAE,KAAK;KAC1B,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAY,EACZ,QAA0D;IAE1D,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAA;IACjD,IAAI,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAA;IAE5C,MAAM,QAAQ,GAAG,KAAK,IAAmB,EAAE;QACzC,MAAM,OAAO,GAAG,QAAQ,EAAE,EAAE,OAAO,IAAI,EAAE,CAAA;QACzC,MAAM,IAAI,GAAG,IAAI,GAAG,EAA2B,CAAA;QAC/C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,SAAQ;YAC7B,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAE,SAAQ;YACnC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC/B,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YAClC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;YACxC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACpC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS;gBAAE,SAAQ;YACxE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;gBAC9B,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACxB,CAAC;YACD,IAAI,MAAwB,CAAA;YAC5B,IAAI,CAAC;gBACH,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;YACjC,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,gDAAgD,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;gBACtF,SAAQ;YACV,CAAC;YACD,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;YAC3C,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;QAC3C,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;YAC1C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpB,MAAM,KAAK,CAAC,OAAO,EAAE,CAAA;gBACrB,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACxB,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IAED,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACpD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,6BAA6B,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QAC/D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,KAAK,IAAmB,EAAE;QACxC,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE;YAC/B,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;gBACtC,IAAI,CAAC;oBAAC,MAAM,KAAK,CAAC,OAAO,EAAE,CAAA;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;YACrE,CAAC;YACD,SAAS,CAAC,KAAK,EAAE,CAAA;QACnB,CAAC,CAAC,CAAA;QACF,MAAM,KAAK,CAAA;IACb,CAAC,CAAA;IAED,IAAI,EAAE,CAAA;IACN,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC1B,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** MCP servers settings schema (host + browser shared). */
|
|
2
|
+
import type { Volatile } from '@deepseek-ai/cordis';
|
|
3
|
+
import z from '@deepseek-ai/schemastery';
|
|
4
|
+
/** Settings namespace owned by the MCP servers settings bridge. */
|
|
5
|
+
export declare const MCP_SERVERS_NAMESPACE = "mcp-servers";
|
|
6
|
+
/** One MCP server declaration as stored in the user-settings document. */
|
|
7
|
+
export interface McpServerConfig {
|
|
8
|
+
name: string;
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
transport: 'stdio' | 'streamable-http';
|
|
13
|
+
url: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Durable MCP servers section shared by the Host schema and the browser scope. */
|
|
17
|
+
export interface McpServersSettings {
|
|
18
|
+
servers: McpServerConfig[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Durable MCP servers schema. `servers` is volatile so the profile-config
|
|
22
|
+
* settings model exposes it as a live-editable field (the plugin is not
|
|
23
|
+
* remounted on edit; the bridge re-syncs on `loader/volatile-update`).
|
|
24
|
+
*/
|
|
25
|
+
export declare const McpServersSchema: z<Schemastery.ObjectS<NoInfer<{
|
|
26
|
+
servers: z<NoInfer<({
|
|
27
|
+
name?: string | null;
|
|
28
|
+
command?: string | null;
|
|
29
|
+
args?: string[] | null;
|
|
30
|
+
env?: import("@deepseek-ai/cosmokit").Dict<string, string> | null;
|
|
31
|
+
transport?: "stdio" | "streamable-http" | null;
|
|
32
|
+
url?: string | null;
|
|
33
|
+
enabled?: boolean | null;
|
|
34
|
+
} & import("@deepseek-ai/cosmokit").Dict)[]>, NoInfer<Schemastery.ObjectT<NoInfer<{
|
|
35
|
+
name: z<string, string, "defined">;
|
|
36
|
+
command: z<string, string, "plain">;
|
|
37
|
+
args: z<string[], string[], "defined">;
|
|
38
|
+
env: z<import("@deepseek-ai/cosmokit").Dict<string, string>, import("@deepseek-ai/cosmokit").Dict<string, string>, "defined">;
|
|
39
|
+
transport: z<"stdio" | "streamable-http", "stdio" | "streamable-http", "defined">;
|
|
40
|
+
url: z<string, string, "defined">;
|
|
41
|
+
enabled: z<boolean, boolean, "defined">;
|
|
42
|
+
}>>[]>, "volatile-defined">;
|
|
43
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
44
|
+
servers: z<NoInfer<({
|
|
45
|
+
name?: string | null;
|
|
46
|
+
command?: string | null;
|
|
47
|
+
args?: string[] | null;
|
|
48
|
+
env?: import("@deepseek-ai/cosmokit").Dict<string, string> | null;
|
|
49
|
+
transport?: "stdio" | "streamable-http" | null;
|
|
50
|
+
url?: string | null;
|
|
51
|
+
enabled?: boolean | null;
|
|
52
|
+
} & import("@deepseek-ai/cosmokit").Dict)[]>, NoInfer<Schemastery.ObjectT<NoInfer<{
|
|
53
|
+
name: z<string, string, "defined">;
|
|
54
|
+
command: z<string, string, "plain">;
|
|
55
|
+
args: z<string[], string[], "defined">;
|
|
56
|
+
env: z<import("@deepseek-ai/cosmokit").Dict<string, string>, import("@deepseek-ai/cosmokit").Dict<string, string>, "defined">;
|
|
57
|
+
transport: z<"stdio" | "streamable-http", "stdio" | "streamable-http", "defined">;
|
|
58
|
+
url: z<string, string, "defined">;
|
|
59
|
+
enabled: z<boolean, boolean, "defined">;
|
|
60
|
+
}>>[]>, "volatile-defined">;
|
|
61
|
+
}>>, "plain">;
|
|
62
|
+
/** Resolved Host config; the volatile field is read through its live reference. */
|
|
63
|
+
export type McpServersHostConfig = {
|
|
64
|
+
servers: Volatile<McpServerConfig[]>;
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=mcp-servers-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-servers-schema.d.ts","sourceRoot":"","sources":["../../src/mcp-servers-schema.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAE3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,gBAAgB,CAAA;AAElD,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3B,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAAA;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,mFAAmF;AACnF,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,eAAe,EAAE,CAAA;CAC3B;AAYD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAE3B,CAAA;AAEF,mFAAmF;AACnF,MAAM,MAAM,oBAAoB,GAAG;IAAE,OAAO,EAAE,QAAQ,CAAC,eAAe,EAAE,CAAC,CAAA;CAAE,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** MCP servers settings schema (host + browser shared). */
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
/** Settings namespace owned by the MCP servers settings bridge. */
|
|
4
|
+
export const MCP_SERVERS_NAMESPACE = 'mcp-servers';
|
|
5
|
+
const McpServerSchema = z.object({
|
|
6
|
+
name: z.string().required(),
|
|
7
|
+
command: z.string(),
|
|
8
|
+
args: z.array(String).default([]),
|
|
9
|
+
env: z.dict(String).default({}),
|
|
10
|
+
transport: z.union(['stdio', 'streamable-http']).default('stdio'),
|
|
11
|
+
url: z.string().default(''),
|
|
12
|
+
enabled: z.boolean().default(true),
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Durable MCP servers schema. `servers` is volatile so the profile-config
|
|
16
|
+
* settings model exposes it as a live-editable field (the plugin is not
|
|
17
|
+
* remounted on edit; the bridge re-syncs on `loader/volatile-update`).
|
|
18
|
+
*/
|
|
19
|
+
export const McpServersSchema = z.object({
|
|
20
|
+
servers: z.array(McpServerSchema).default([]).volatile(),
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=mcp-servers-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-servers-schema.js","sourceRoot":"","sources":["../../src/mcp-servers-schema.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAG3D,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,mEAAmE;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAG,aAAa,CAAA;AAkBlD,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACjC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACjE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACnC,CAAC,CAAA;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;CACzD,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@railgun52/dsh-mcp-servers",
|
|
3
|
+
"description": "MCP servers settings page for the dsh web GUI: a JSON editor over a profile-config namespace that mounts live mcp-client instances from the saved servers",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"icon": "icon.svg",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"packageManager": "pnpm@11.7.0",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -b && tsdown",
|
|
16
|
+
"bundle": "tsdown",
|
|
17
|
+
"watch": "tsdown --watch",
|
|
18
|
+
"typecheck": "tsc -b --pretty false"
|
|
19
|
+
},
|
|
20
|
+
"main": "lib/index.js",
|
|
21
|
+
"types": "lib/types/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./lib/types/index.d.ts",
|
|
25
|
+
"default": "./lib/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./invariant": {
|
|
28
|
+
"types": "./lib/types/invariant.d.ts",
|
|
29
|
+
"default": "./lib/invariant.js"
|
|
30
|
+
},
|
|
31
|
+
"./client": {
|
|
32
|
+
"types": "./lib/types/client/index.d.ts",
|
|
33
|
+
"default": "./lib/client.js"
|
|
34
|
+
},
|
|
35
|
+
"./src/*": "./src/*",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"lib/**/*.js",
|
|
40
|
+
"lib/**/*.js.map",
|
|
41
|
+
"lib/**/*.d.ts",
|
|
42
|
+
"lib/**/*.d.ts.map",
|
|
43
|
+
"src",
|
|
44
|
+
"cordis.patch.yml",
|
|
45
|
+
"icon.svg"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dsh": {
|
|
49
|
+
"engines": {
|
|
50
|
+
"dsh": ">=0.1.7-rc.1"
|
|
51
|
+
},
|
|
52
|
+
"bundle": {
|
|
53
|
+
"patch": "./cordis.patch.yml"
|
|
54
|
+
},
|
|
55
|
+
"client": {
|
|
56
|
+
"inject": [
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
58
|
+
"@deepseek-ai/dsh-client-locale",
|
|
59
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
60
|
+
],
|
|
61
|
+
"platform": "web"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.1",
|
|
69
|
+
"react": "^18.2.0",
|
|
70
|
+
"react-dom": "^18.2.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
74
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-mcp-client": "^0.1.7-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
|
|
84
|
+
"@types/react": "~18.3.1",
|
|
85
|
+
"lightningcss": "^1.32.0",
|
|
86
|
+
"react": "^18.2.0",
|
|
87
|
+
"react-dom": "^18.2.0",
|
|
88
|
+
"tsdown": "^0.22.2",
|
|
89
|
+
"typescript": "^6.0.3",
|
|
90
|
+
"vite-tsconfig-paths": "^6.1.1"
|
|
91
|
+
},
|
|
92
|
+
"repository": {
|
|
93
|
+
"type": "git",
|
|
94
|
+
"url": "https://github.com/railgun52/dsh-mcp-servers.git"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
.section {
|
|
2
|
+
padding: 24px;
|
|
3
|
+
max-width: 800px;
|
|
4
|
+
}
|
|
5
|
+
.header {
|
|
6
|
+
margin-bottom: 24px;
|
|
7
|
+
}
|
|
8
|
+
.title {
|
|
9
|
+
font-size: 20px;
|
|
10
|
+
font-weight: 600;
|
|
11
|
+
margin: 0 0 8px;
|
|
12
|
+
}
|
|
13
|
+
.intro {
|
|
14
|
+
color: var(--text-secondary);
|
|
15
|
+
margin: 0;
|
|
16
|
+
font-size: 14px;
|
|
17
|
+
}
|
|
18
|
+
.toolbar {
|
|
19
|
+
margin-bottom: 16px;
|
|
20
|
+
}
|
|
21
|
+
.list {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
}
|
|
26
|
+
.card {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
padding: 12px 16px;
|
|
31
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
32
|
+
border-radius: 10px;
|
|
33
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
34
|
+
transition: box-shadow 0.15s;
|
|
35
|
+
}
|
|
36
|
+
.card:hover {
|
|
37
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
38
|
+
}
|
|
39
|
+
.cardBody {
|
|
40
|
+
flex: 1;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
}
|
|
43
|
+
.cardName {
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
}
|
|
49
|
+
.cardMeta {
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
color: var(--text-secondary);
|
|
52
|
+
margin-top: 4px;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
text-overflow: ellipsis;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
}
|
|
57
|
+
.cardMeta code {
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
}
|
|
60
|
+
.cardActions {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: 4px;
|
|
63
|
+
align-items: center;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
|
66
|
+
.statusOn {
|
|
67
|
+
width: 8px;
|
|
68
|
+
height: 8px;
|
|
69
|
+
border-radius: 50%;
|
|
70
|
+
background: #22c55e;
|
|
71
|
+
display: inline-block;
|
|
72
|
+
}
|
|
73
|
+
.statusOff {
|
|
74
|
+
width: 8px;
|
|
75
|
+
height: 8px;
|
|
76
|
+
border-radius: 50%;
|
|
77
|
+
background: #888;
|
|
78
|
+
display: inline-block;
|
|
79
|
+
}
|
|
80
|
+
.deleteBtn {
|
|
81
|
+
background: none;
|
|
82
|
+
border: none;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
padding: 4px;
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
color: var(--text-secondary);
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
}
|
|
90
|
+
.deleteBtn:hover {
|
|
91
|
+
background: var(--bg-hover);
|
|
92
|
+
color: var(--text-error);
|
|
93
|
+
}
|
|
94
|
+
.empty {
|
|
95
|
+
text-align: center;
|
|
96
|
+
padding: 40px;
|
|
97
|
+
color: var(--text-secondary);
|
|
98
|
+
}
|
|
99
|
+
.error {
|
|
100
|
+
padding: 8px 12px;
|
|
101
|
+
background: var(--bg-error);
|
|
102
|
+
color: var(--text-error);
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
margin-bottom: 16px;
|
|
105
|
+
font-size: 13px;
|
|
106
|
+
}
|
|
107
|
+
.loading {
|
|
108
|
+
text-align: center;
|
|
109
|
+
padding: 40px;
|
|
110
|
+
color: var(--text-secondary);
|
|
111
|
+
}
|
|
112
|
+
.modalBody {
|
|
113
|
+
padding: 0;
|
|
114
|
+
}
|
|
115
|
+
.modalBody p {
|
|
116
|
+
padding: 0 16px;
|
|
117
|
+
}
|
|
118
|
+
.modalActions {
|
|
119
|
+
display: flex;
|
|
120
|
+
gap: 8px;
|
|
121
|
+
padding: 16px;
|
|
122
|
+
justify-content: flex-end;
|
|
123
|
+
}
|
|
124
|
+
.jsonEditor {
|
|
125
|
+
width: 100%;
|
|
126
|
+
padding: 12px;
|
|
127
|
+
border: none;
|
|
128
|
+
font-family: monospace;
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
line-height: 1.5;
|
|
131
|
+
resize: vertical;
|
|
132
|
+
background: var(--bg-input);
|
|
133
|
+
color: var(--text-primary);
|
|
134
|
+
box-sizing: border-box;
|
|
135
|
+
min-height: 300px;
|
|
136
|
+
outline: none;
|
|
137
|
+
}
|
|
138
|
+
.jsonEditor:focus {
|
|
139
|
+
background: var(--bg-input-focus);
|
|
140
|
+
}
|
|
141
|
+
.dialogMcp {
|
|
142
|
+
width: min(720px, 100%);
|
|
143
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { Button, IconPlusOutlineRegular, IconTrashOutlineRegular, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
import type { McpKey } from './locales.ts'
|
|
4
|
+
import styles from './McpSection.module.css'
|
|
5
|
+
|
|
6
|
+
interface McpServer {
|
|
7
|
+
name: string
|
|
8
|
+
command: string
|
|
9
|
+
args: string[]
|
|
10
|
+
env: Record<string, string>
|
|
11
|
+
transport: 'stdio' | 'streamable-http'
|
|
12
|
+
url: string
|
|
13
|
+
enabled: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface McpSectionInjected {
|
|
17
|
+
load: () => unknown[]
|
|
18
|
+
save: (servers: unknown[]) => Promise<void>
|
|
19
|
+
t: (key: McpKey) => string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type McpSectionProps = Partial<McpSectionInjected>
|
|
23
|
+
|
|
24
|
+
function serversToJson(servers: McpServer[]): string {
|
|
25
|
+
const mcpServers: Record<string, Record<string, unknown>> = {}
|
|
26
|
+
for (const s of servers) {
|
|
27
|
+
const entry: Record<string, unknown> = { command: s.command }
|
|
28
|
+
if (s.args.length > 0) entry.args = s.args
|
|
29
|
+
if (Object.keys(s.env).length > 0) entry.env = s.env
|
|
30
|
+
if (s.transport === 'streamable-http') entry.transport = 'streamable-http'
|
|
31
|
+
if (s.url) entry.url = s.url
|
|
32
|
+
mcpServers[s.name] = entry
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify({ mcpServers }, null, 2)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function jsonToServers(text: string): McpServer[] {
|
|
38
|
+
const parsed = JSON.parse(text)
|
|
39
|
+
const map = parsed.mcpServers
|
|
40
|
+
if (!map || typeof map !== 'object') throw new Error('Missing "mcpServers" key')
|
|
41
|
+
return Object.entries(map).map(([name, config]) => {
|
|
42
|
+
const c = config as Record<string, unknown>
|
|
43
|
+
const args = Array.isArray(c.args) ? c.args.map(String) : []
|
|
44
|
+
const env = c.env && typeof c.env === 'object'
|
|
45
|
+
? Object.fromEntries(Object.entries(c.env).map(([k, v]) => [k, String(v)]))
|
|
46
|
+
: {}
|
|
47
|
+
return {
|
|
48
|
+
name,
|
|
49
|
+
command: String(c.command ?? ''),
|
|
50
|
+
args,
|
|
51
|
+
env,
|
|
52
|
+
transport: c.transport === 'streamable-http' ? 'streamable-http' as const : 'stdio' as const,
|
|
53
|
+
url: String(c.url ?? ''),
|
|
54
|
+
enabled: c.enabled !== false,
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const EMPTY_TEMPLATE = JSON.stringify({
|
|
60
|
+
mcpServers: {
|
|
61
|
+
'my-server': {
|
|
62
|
+
command: 'npx',
|
|
63
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', '/path'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}, null, 2)
|
|
67
|
+
|
|
68
|
+
export function McpSection({ load, save, t: t_ }: McpSectionProps) {
|
|
69
|
+
const t = t_ ?? ((key: string) => key)
|
|
70
|
+
const [servers, setServers] = useState<McpServer[]>([])
|
|
71
|
+
const [loading, setLoading] = useState(true)
|
|
72
|
+
const [saving, setSaving] = useState(false)
|
|
73
|
+
const [error, setError] = useState<string | null>(null)
|
|
74
|
+
const [editJson, setEditJson] = useState('')
|
|
75
|
+
const [editIndex, setEditIndex] = useState<number | null>(null)
|
|
76
|
+
const [showEditModal, setShowEditModal] = useState(false)
|
|
77
|
+
const [editError, setEditError] = useState<string | null>(null)
|
|
78
|
+
const [deleteTarget, setDeleteTarget] = useState<{ name: string; index: number } | null>(null)
|
|
79
|
+
|
|
80
|
+
const loadConfig = useCallback(() => {
|
|
81
|
+
setLoading(true)
|
|
82
|
+
setError(null)
|
|
83
|
+
try {
|
|
84
|
+
const loaded = load?.() ?? []
|
|
85
|
+
setServers(loaded as McpServer[])
|
|
86
|
+
} catch (e) {
|
|
87
|
+
setError(t('loadFailed'))
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(false)
|
|
90
|
+
}
|
|
91
|
+
}, [load, t])
|
|
92
|
+
|
|
93
|
+
useEffect(() => { loadConfig() }, [loadConfig])
|
|
94
|
+
|
|
95
|
+
const saveConfig = async (newServers: McpServer[]) => {
|
|
96
|
+
setSaving(true)
|
|
97
|
+
setError(null)
|
|
98
|
+
try {
|
|
99
|
+
await save?.(newServers)
|
|
100
|
+
setServers(newServers)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
setError(t('saveFailed'))
|
|
103
|
+
} finally {
|
|
104
|
+
setSaving(false)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const handleAdd = () => {
|
|
109
|
+
setEditJson(EMPTY_TEMPLATE)
|
|
110
|
+
setEditIndex(null)
|
|
111
|
+
setEditError(null)
|
|
112
|
+
setShowEditModal(true)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const handleEdit = (index: number) => {
|
|
116
|
+
const server = servers[index]
|
|
117
|
+
if (!server) return
|
|
118
|
+
setEditJson(serversToJson([server]))
|
|
119
|
+
setEditIndex(index)
|
|
120
|
+
setEditError(null)
|
|
121
|
+
setShowEditModal(true)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const handleSaveJson = async () => {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = jsonToServers(editJson)
|
|
127
|
+
if (parsed.length === 0) {
|
|
128
|
+
setEditError('No servers found in JSON')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if (editIndex !== null) {
|
|
132
|
+
const newServers = [...servers]
|
|
133
|
+
newServers[editIndex] = parsed[0]!
|
|
134
|
+
await saveConfig(newServers)
|
|
135
|
+
} else {
|
|
136
|
+
const merged = new Map(servers.map(s => [s.name, s]))
|
|
137
|
+
for (const s of parsed) merged.set(s.name, s)
|
|
138
|
+
await saveConfig(Array.from(merged.values()).filter((s): s is McpServer => s !== undefined))
|
|
139
|
+
}
|
|
140
|
+
setShowEditModal(false)
|
|
141
|
+
} catch (e) {
|
|
142
|
+
setEditError(e instanceof Error ? e.message : String(e))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handleDelete = async (index: number) => {
|
|
147
|
+
const newServers = servers.filter((_, i) => i !== index)
|
|
148
|
+
await saveConfig(newServers)
|
|
149
|
+
setDeleteTarget(null)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (loading) {
|
|
153
|
+
return <div className={styles.section}><div className={styles.loading}>Loading...</div></div>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={styles.section}>
|
|
158
|
+
<div className={styles.header}>
|
|
159
|
+
<h2 className={styles.title}>{t('title')}</h2>
|
|
160
|
+
<p className={styles.intro}>{t('intro')}</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{error && <div className={styles.error}>{error}</div>}
|
|
164
|
+
|
|
165
|
+
<div className={styles.toolbar}>
|
|
166
|
+
<Button onClick={handleAdd} disabled={saving}>
|
|
167
|
+
<IconPlusOutlineRegular /> {t('add')}
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{servers.length === 0 && (
|
|
172
|
+
<div className={styles.empty}>{t('noServers')}</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
<div className={styles.list}>
|
|
176
|
+
{servers.map((server, i) => (
|
|
177
|
+
<div key={i} className={styles.card}>
|
|
178
|
+
<div className={styles.cardBody}>
|
|
179
|
+
<div className={styles.cardName}>
|
|
180
|
+
<span className={server.enabled ? styles.statusOn : styles.statusOff} />
|
|
181
|
+
{server.name}
|
|
182
|
+
</div>
|
|
183
|
+
<div className={styles.cardMeta}>
|
|
184
|
+
<code>{server.command} {server.args.join(' ')}</code>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div className={styles.cardActions}>
|
|
188
|
+
<Button onClick={() => handleEdit(i)}>{t('edit')}</Button>
|
|
189
|
+
<button className={styles.deleteBtn} onClick={() => setDeleteTarget({ name: server.name, index: i })}>
|
|
190
|
+
<IconTrashOutlineRegular />
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{showEditModal && (
|
|
198
|
+
<Modal
|
|
199
|
+
open
|
|
200
|
+
onClose={() => setShowEditModal(false)}
|
|
201
|
+
title={editIndex !== null ? t('edit') : t('add')}
|
|
202
|
+
closeLabel={t('close')}
|
|
203
|
+
className={styles.dialogMcp ?? ''}
|
|
204
|
+
footer={(
|
|
205
|
+
<>
|
|
206
|
+
<Button onClick={handleSaveJson} disabled={saving}>{t('save')}</Button>
|
|
207
|
+
<Button onClick={() => setShowEditModal(false)}>{t('cancel')}</Button>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<div className={styles.modalBody}>
|
|
212
|
+
{editError && <div className={styles.error}>{editError}</div>}
|
|
213
|
+
<textarea
|
|
214
|
+
className={styles.jsonEditor}
|
|
215
|
+
value={editJson}
|
|
216
|
+
onChange={e => setEditJson(e.target.value)}
|
|
217
|
+
rows={16}
|
|
218
|
+
spellCheck={false}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
</Modal>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{deleteTarget && (
|
|
225
|
+
<Modal
|
|
226
|
+
open
|
|
227
|
+
onClose={() => setDeleteTarget(null)}
|
|
228
|
+
title={t('remove')}
|
|
229
|
+
closeLabel={t('close')}
|
|
230
|
+
className={styles.dialogMcp ?? ''}
|
|
231
|
+
footer={(
|
|
232
|
+
<>
|
|
233
|
+
<Button onClick={() => handleDelete(deleteTarget.index)}>{t('remove')}</Button>
|
|
234
|
+
<Button onClick={() => setDeleteTarget(null)}>{t('cancel')}</Button>
|
|
235
|
+
</>
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
238
|
+
<div className={styles.modalBody}>
|
|
239
|
+
<p>{t('deleteConfirm').replace('{server}', deleteTarget.name)}</p>
|
|
240
|
+
<p>{t('deleteDescription')}</p>
|
|
241
|
+
</div>
|
|
242
|
+
</Modal>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|