@khorsheed/dsh-capability-catalog 0.1.95

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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +286 -0
  4. package/README.zh.md +138 -0
  5. package/cordis.patch.yml +8 -0
  6. package/lib/client.js +10100 -0
  7. package/lib/index.js +4022 -0
  8. package/lib/invariant.js +25 -0
  9. package/lib/mcpConnector-C2TbkPBY.js +69 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +1417 -0
  12. package/lib/typert.remote-client.d.ts +68 -0
  13. package/lib/typert.remote-client.js +1136 -0
  14. package/lib/types/capabilities.d.ts +103 -0
  15. package/lib/types/capabilities.js +116 -0
  16. package/lib/types/channels.d.ts +41 -0
  17. package/lib/types/channels.js +75 -0
  18. package/lib/types/client/AddMcpDialog.d.ts +13 -0
  19. package/lib/types/client/AddMcpDialog.js +79 -0
  20. package/lib/types/client/AddSkillModal.d.ts +17 -0
  21. package/lib/types/client/AddSkillModal.js +138 -0
  22. package/lib/types/client/BundleFileTree.d.ts +7 -0
  23. package/lib/types/client/BundleFileTree.js +74 -0
  24. package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
  25. package/lib/types/client/CapabilityCatalogCard.js +479 -0
  26. package/lib/types/client/CredentialField.d.ts +14 -0
  27. package/lib/types/client/CredentialField.js +9 -0
  28. package/lib/types/client/McpCards.d.ts +13 -0
  29. package/lib/types/client/McpCards.js +12 -0
  30. package/lib/types/client/McpServerManageModal.d.ts +18 -0
  31. package/lib/types/client/McpServerManageModal.js +63 -0
  32. package/lib/types/client/MetadataRow.d.ts +10 -0
  33. package/lib/types/client/MetadataRow.js +6 -0
  34. package/lib/types/client/ModalShell.d.ts +16 -0
  35. package/lib/types/client/ModalShell.js +27 -0
  36. package/lib/types/client/ModeChips.d.ts +38 -0
  37. package/lib/types/client/ModeChips.js +42 -0
  38. package/lib/types/client/SchemaView.d.ts +11 -0
  39. package/lib/types/client/SchemaView.js +143 -0
  40. package/lib/types/client/SkillCards.d.ts +28 -0
  41. package/lib/types/client/SkillCards.js +43 -0
  42. package/lib/types/client/SkillDetailModal.d.ts +69 -0
  43. package/lib/types/client/SkillDetailModal.js +167 -0
  44. package/lib/types/client/ToolCards.d.ts +28 -0
  45. package/lib/types/client/ToolCards.js +52 -0
  46. package/lib/types/client/ToolDetailModal.d.ts +12 -0
  47. package/lib/types/client/ToolDetailModal.js +29 -0
  48. package/lib/types/client/icons.d.ts +26 -0
  49. package/lib/types/client/icons.js +10 -0
  50. package/lib/types/client/index.d.ts +21 -0
  51. package/lib/types/client/index.js +196 -0
  52. package/lib/types/client/locales.d.ts +392 -0
  53. package/lib/types/client/locales.js +395 -0
  54. package/lib/types/client/mcp-model.d.ts +24 -0
  55. package/lib/types/client/mcp-model.js +87 -0
  56. package/lib/types/client/mode-model.d.ts +108 -0
  57. package/lib/types/client/mode-model.js +140 -0
  58. package/lib/types/client/preset-display.d.ts +21 -0
  59. package/lib/types/client/preset-display.js +20 -0
  60. package/lib/types/client/settle.d.ts +40 -0
  61. package/lib/types/client/settle.js +57 -0
  62. package/lib/types/client/slots.d.ts +86 -0
  63. package/lib/types/client/slots.js +1 -0
  64. package/lib/types/envHint.d.ts +22 -0
  65. package/lib/types/envHint.js +76 -0
  66. package/lib/types/import.d.ts +106 -0
  67. package/lib/types/import.js +635 -0
  68. package/lib/types/index.d.ts +216 -0
  69. package/lib/types/index.js +739 -0
  70. package/lib/types/invariant.d.ts +16 -0
  71. package/lib/types/invariant.js +23 -0
  72. package/lib/types/mcpConnector.d.ts +24 -0
  73. package/lib/types/mcpConnector.js +73 -0
  74. package/lib/types/mcpStore.d.ts +68 -0
  75. package/lib/types/mcpStore.js +235 -0
  76. package/lib/types/mcpTools.d.ts +45 -0
  77. package/lib/types/mcpTools.js +125 -0
  78. package/lib/types/mcps.d.ts +64 -0
  79. package/lib/types/mcps.js +157 -0
  80. package/lib/types/modes.d.ts +65 -0
  81. package/lib/types/modes.js +97 -0
  82. package/lib/types/namespace.d.ts +9 -0
  83. package/lib/types/namespace.js +8 -0
  84. package/lib/types/official-tools.d.ts +3 -0
  85. package/lib/types/official-tools.js +44 -0
  86. package/lib/types/preset-scope.d.ts +105 -0
  87. package/lib/types/preset-scope.js +117 -0
  88. package/lib/types/remote.d.ts +48 -0
  89. package/lib/types/remote.js +120 -0
  90. package/lib/types/scoped-delivery.d.ts +243 -0
  91. package/lib/types/scoped-delivery.js +686 -0
  92. package/lib/types/scoped-edits.d.ts +90 -0
  93. package/lib/types/scoped-edits.js +234 -0
  94. package/lib/types/settings.d.ts +28 -0
  95. package/lib/types/settings.js +28 -0
  96. package/lib/types/shellEnv.d.ts +65 -0
  97. package/lib/types/shellEnv.js +232 -0
  98. package/lib/types/skills.d.ts +103 -0
  99. package/lib/types/skills.js +303 -0
  100. package/lib/types/tool-origin.d.ts +29 -0
  101. package/lib/types/tool-origin.js +30 -0
  102. package/lib/types/types.d.ts +315 -0
  103. package/lib/types/types.js +8 -0
  104. package/lib/types/zip.d.ts +24 -0
  105. package/lib/types/zip.js +80 -0
  106. package/locale/en.json +6 -0
  107. package/locale/zh.json +6 -0
  108. package/package.json +119 -0
  109. package/scripts/gen-official-tools.mts +52 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-capability-catalog`.
3
+ * @module @khorsheed/dsh-capability-catalog/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "capability-catalog-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-capability-catalog`.
3
+ * @module @khorsheed/dsh-capability-catalog/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-capability-catalog';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'capability-catalog-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: the catalog is read-only over existing registries
12
+ * (ctx.skills / ctx.tools) and the add-skill write path validates its inputs
13
+ * before touching disk. The unit tests exercise the channel attribution,
14
+ * skill decoding, and the wire projection.
15
+ */
16
+ const install = () => { };
17
+ /**
18
+ * Register this package's invariant companion.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
23
+ /* jscpd:ignore-end */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * MCP connector backed by the official `@modelcontextprotocol/sdk` (the same
3
+ * library `@deepseek-ai/dsh-mcp-client` uses). Kept as a thin, swappable seam:
4
+ * the catalog owns the connection lifecycle (connect → list → register tools →
5
+ * call → close) and registers discovered tools on `ctx.tools`. If upstream later
6
+ * exposes a stable dynamic `connect(ctx, config)`, swap this adapter for it.
7
+ *
8
+ * v1 scope: stdio + streamable-http connect, paginated `tools/list`,
9
+ * `tools/call` passthrough, and close. Reconnect/atomic-replace/tool-filter are
10
+ * follow-ups.
11
+ * @module @khorsheed/dsh-capability-catalog/mcpConnector
12
+ */
13
+ import type { CatalogMcpServerConfig, CatalogMcpTool } from './types.ts';
14
+ /** One live MCP connection. */
15
+ export interface McpConnection {
16
+ readonly tools: readonly CatalogMcpTool[];
17
+ call: (toolName: string, args: Record<string, unknown>) => Promise<unknown>;
18
+ close: () => Promise<void>;
19
+ }
20
+ /** Resolve a secretRef marker to its stored value (the catalog's ctx.credentials). */
21
+ export type ResolveSecret = (ref: string) => Promise<string | undefined>;
22
+ /** Connect, discover all tools (follow pagination), and return a live handle. */
23
+ export declare function connectMcpServer(config: CatalogMcpServerConfig): Promise<McpConnection>;
24
+ //# sourceMappingURL=mcpConnector.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * MCP connector backed by the official `@modelcontextprotocol/sdk` (the same
3
+ * library `@deepseek-ai/dsh-mcp-client` uses). Kept as a thin, swappable seam:
4
+ * the catalog owns the connection lifecycle (connect → list → register tools →
5
+ * call → close) and registers discovered tools on `ctx.tools`. If upstream later
6
+ * exposes a stable dynamic `connect(ctx, config)`, swap this adapter for it.
7
+ *
8
+ * v1 scope: stdio + streamable-http connect, paginated `tools/list`,
9
+ * `tools/call` passthrough, and close. Reconnect/atomic-replace/tool-filter are
10
+ * follow-ups.
11
+ * @module @khorsheed/dsh-capability-catalog/mcpConnector
12
+ */
13
+ // The SDK is a third-party host dep the gen-typert overlay cannot resolve
14
+ // (only @deepseek-ai/* + harness node_modules). @ts-ignore keeps generation
15
+ // green; at runtime the host resolves these from node_modules.
16
+ // @ts-ignore
17
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
+ // @ts-ignore
19
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
20
+ // @ts-ignore
21
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
22
+ /** Build the transport for a config (secrets already resolved to plaintext). */
23
+ function makeTransport(config) {
24
+ if (config.transport === 'stdio') {
25
+ const env = {};
26
+ for (const [k, v] of config.env ?? [])
27
+ env[k] = v;
28
+ return new StdioClientTransport({
29
+ command: config.command ?? 'npx',
30
+ args: [...(config.args ?? [])],
31
+ ...(Object.keys(env).length > 0 ? { env } : {}),
32
+ ...(config.cwd ? { cwd: config.cwd } : {}),
33
+ });
34
+ }
35
+ // streamable-http: the SDK transport takes headers via `requestInit` — without
36
+ // them an auth-bearing server (e.g. `Authorization`, `x-api-key`) is never
37
+ // authenticated. Plaintext header values arrive here after secret resolution.
38
+ const headers = config.headers ?? [];
39
+ if (headers.length > 0) {
40
+ return new StreamableHTTPClientTransport(new URL(config.url ?? ''), {
41
+ requestInit: { headers: Object.fromEntries(headers) },
42
+ });
43
+ }
44
+ return new StreamableHTTPClientTransport(new URL(config.url ?? ''));
45
+ }
46
+ /** Map an SDK tool to the catalog's tool row (enabled by default). */
47
+ function toCatalogTool(sdkTool) {
48
+ return {
49
+ name: sdkTool.name,
50
+ description: sdkTool.description ?? '',
51
+ ...(sdkTool.inputSchema !== undefined ? { parameters: sdkTool.inputSchema } : {}),
52
+ enabled: true,
53
+ };
54
+ }
55
+ /** Connect, discover all tools (follow pagination), and return a live handle. */
56
+ export async function connectMcpServer(config) {
57
+ const client = new Client({ name: 'dsh-capability-catalog', version: '0.1.0' });
58
+ const transport = makeTransport(config);
59
+ await client.connect(transport);
60
+ const tools = [];
61
+ let cursor;
62
+ do {
63
+ const page = await client.listTools(cursor === undefined ? undefined : { cursor });
64
+ for (const t of page.tools)
65
+ tools.push(toCatalogTool(t));
66
+ cursor = page.nextCursor;
67
+ } while (cursor !== undefined);
68
+ return {
69
+ tools,
70
+ call: async (toolName, args) => client.callTool({ name: toolName, arguments: args }),
71
+ close: async () => { await client.close().catch(() => { }); },
72
+ };
73
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Host-side MCP server store: keeps the user's configured MCP servers, their
3
+ * discovered tools, credential state, and enable flags. Persistence is a pluggable
4
+ * seam: the host hands the store an `onPersist` callback (backed by the settings
5
+ * namespace) and seeds it with `loadFrom` at boot, so servers survive a restart.
6
+ * Tool discovery connects via the SDK connector and caches the tool list.
7
+ */
8
+ import type { CatalogMcpCredentialDecl, CatalogMcpServerConfig, CatalogMcpTool } from './types.ts';
9
+ /** One tool's persisted record: the user's toggle choice only. Descriptions and
10
+ * parameter schemas are discovered at connect time and deliberately NOT persisted
11
+ * (they bloat settings.yaml and go stale); `reconnectAll` re-fills them at boot. */
12
+ export interface PersistedMcpTool {
13
+ readonly name: string;
14
+ readonly enabled: boolean;
15
+ }
16
+ /** Wire/persisted form of one stored server (config + enabled + tool toggles). */
17
+ export interface PersistedMcpServer {
18
+ readonly config: CatalogMcpServerConfig;
19
+ readonly tools: readonly PersistedMcpTool[];
20
+ }
21
+ /** The full persisted MCP state block (stored under the settings namespace `mcp` key). */
22
+ export interface PersistedMcpState {
23
+ readonly servers?: readonly PersistedMcpServer[];
24
+ /** Namespaced credential ref -> value (the user's own config document). */
25
+ readonly credentials?: Readonly<Record<string, string>>;
26
+ }
27
+ /** The catalog's own MCP management store (one per process). */
28
+ export declare class McpStore {
29
+ private readonly servers;
30
+ /** Persistence hook, set by the host after the settings scope is available. */
31
+ onPersist: (() => void) | undefined;
32
+ /** Seed the store from the persisted state (boot or external config edit). */
33
+ loadFrom(state: PersistedMcpState | undefined): void;
34
+ /** Serialize the current state for persistence. */
35
+ toPersisted(): PersistedMcpState;
36
+ private persist;
37
+ list(): CatalogMcpServerConfig[];
38
+ /** Discovered tool list per server (name → tools). */
39
+ toolsByServer(): Record<string, readonly CatalogMcpTool[]>;
40
+ /** Credential declarations for all servers (whether configured, never the value). */
41
+ credentials(): CatalogMcpCredentialDecl[];
42
+ /** Add / replace a server config (keeps its credentials if already present). */
43
+ add(config: CatalogMcpServerConfig): void;
44
+ remove(serverName: string): void;
45
+ /** Set server-level enable flag (discovery/registration is the caller's job). */
46
+ setEnabled(serverName: string, enabled: boolean): void;
47
+ /** Set one credential value (empty clears it). */
48
+ setCredential(ref: string, value: string): void;
49
+ /** Configure a tool's enable flag. */
50
+ setToolEnabled(serverName: string, toolName: string, enabled: boolean): void;
51
+ /** Connect + discover tools for a server; caches the list (connect errors surface). */
52
+ discover(serverName: string): Promise<readonly CatalogMcpTool[]>;
53
+ /** Re-connect + re-discover every enabled server — boot-time refill of the
54
+ * discovered descriptions/parameters (the persisted block stores only
55
+ * name+enabled). Preserves per-tool enable flags; per-server errors are
56
+ * recorded and do not stop the others. */
57
+ reconnectAll(): Promise<void>;
58
+ /** A server's most recent connection/dev error. */
59
+ errorOf(serverName: string): string | undefined;
60
+ /** Forward one tool call to a server (connect → call → close). Resolves secrets. */
61
+ callTool(serverName: string, rawName: string, args: Record<string, unknown>): Promise<unknown>;
62
+ /** Extract credential reference paths from a config (env/header/url-query).
63
+ * Namespaced `mcp.<serverName>.env.<KEY>` so multiple servers don't collide. */
64
+ private credentialRefsOf;
65
+ }
66
+ /** Whether a config value contains an unresolved secret marker (bare or scheme-prefixed). */
67
+ export declare function isSecretRef(value: string): boolean;
68
+ //# sourceMappingURL=mcpStore.d.ts.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Host-side MCP server store: keeps the user's configured MCP servers, their
3
+ * discovered tools, credential state, and enable flags. Persistence is a pluggable
4
+ * seam: the host hands the store an `onPersist` callback (backed by the settings
5
+ * namespace) and seeds it with `loadFrom` at boot, so servers survive a restart.
6
+ * Tool discovery connects via the SDK connector and caches the tool list.
7
+ */
8
+ import { resolveConfigSecrets } from './mcps.js';
9
+ /** The catalog's own MCP management store (one per process). */
10
+ export class McpStore {
11
+ servers = new Map();
12
+ /** Persistence hook, set by the host after the settings scope is available. */
13
+ onPersist = undefined;
14
+ /** Seed the store from the persisted state (boot or external config edit). */
15
+ loadFrom(state) {
16
+ if (state === undefined)
17
+ return;
18
+ for (const server of state.servers ?? []) {
19
+ const existing = this.servers.get(server.config.serverName);
20
+ // Keep descriptions/parameters already discovered in memory: the persisted
21
+ // block is authoritative only for the per-tool enabled flag (it stores just
22
+ // name+enabled), so a reload — e.g. after our own persist round-trips through
23
+ // the settings watch — must merge the toggle rather than wipe the tools.
24
+ const known = new Map((existing?.tools ?? []).map(t => [t.name, t]));
25
+ const tools = (server.tools ?? []).map(t => {
26
+ const prev = known.get(t.name);
27
+ return prev !== undefined
28
+ ? { ...prev, enabled: t.enabled }
29
+ : { name: t.name, description: '', enabled: t.enabled };
30
+ });
31
+ this.servers.set(server.config.serverName, {
32
+ config: server.config,
33
+ credentials: existing?.credentials ?? new Map(),
34
+ tools,
35
+ error: undefined,
36
+ });
37
+ }
38
+ for (const [ref, value] of Object.entries(state.credentials ?? {})) {
39
+ const s = this.servers.get(refServer(ref));
40
+ if (s !== undefined)
41
+ s.credentials.set(ref, value);
42
+ }
43
+ }
44
+ /** Serialize the current state for persistence. */
45
+ toPersisted() {
46
+ const servers = [...this.servers.values()]
47
+ .sort((a, b) => a.config.serverName.localeCompare(b.config.serverName))
48
+ .map(s => ({ config: s.config, tools: s.tools.map(t => ({ name: t.name, enabled: t.enabled })) }));
49
+ const credentials = {};
50
+ for (const s of this.servers.values())
51
+ for (const [ref, value] of s.credentials)
52
+ credentials[ref] = value;
53
+ return { servers, credentials };
54
+ }
55
+ persist() {
56
+ this.onPersist?.();
57
+ }
58
+ list() {
59
+ return [...this.servers.values()].map(s => ({ ...s.config, enabled: s.config.enabled })).sort((a, b) => a.serverName.localeCompare(b.serverName));
60
+ }
61
+ /** Discovered tool list per server (name → tools). */
62
+ toolsByServer() {
63
+ const out = {};
64
+ for (const [name, s] of this.servers)
65
+ out[name] = s.tools;
66
+ return out;
67
+ }
68
+ /** Credential declarations for all servers (whether configured, never the value). */
69
+ credentials() {
70
+ const out = [];
71
+ for (const [name, s] of this.servers) {
72
+ for (const ref of this.credentialRefsOf(s.config)) {
73
+ out.push({ ref, label: refLabel(ref), configured: s.credentials.has(ref) });
74
+ }
75
+ void name;
76
+ }
77
+ return out;
78
+ }
79
+ /** Add / replace a server config (keeps its credentials if already present). */
80
+ add(config) {
81
+ const existing = this.servers.get(config.serverName);
82
+ this.servers.set(config.serverName, {
83
+ config,
84
+ credentials: existing?.credentials ?? new Map(),
85
+ tools: existing?.tools ?? [],
86
+ error: existing?.error,
87
+ });
88
+ this.persist();
89
+ }
90
+ remove(serverName) {
91
+ if (this.servers.delete(serverName))
92
+ this.persist();
93
+ }
94
+ /** Set server-level enable flag (discovery/registration is the caller's job). */
95
+ setEnabled(serverName, enabled) {
96
+ const s = this.servers.get(serverName);
97
+ if (s === undefined)
98
+ return;
99
+ s.config = { ...s.config, enabled };
100
+ this.persist();
101
+ }
102
+ /** Set one credential value (empty clears it). */
103
+ setCredential(ref, value) {
104
+ const serverName = refServer(ref);
105
+ const s = this.servers.get(serverName);
106
+ if (s === undefined)
107
+ return;
108
+ if (value === '')
109
+ s.credentials.delete(ref);
110
+ else
111
+ s.credentials.set(ref, value);
112
+ this.persist();
113
+ }
114
+ /** Configure a tool's enable flag. */
115
+ setToolEnabled(serverName, toolName, enabled) {
116
+ const s = this.servers.get(serverName);
117
+ if (s === undefined)
118
+ return;
119
+ s.tools = s.tools.map(t => (t.name === toolName ? { ...t, enabled } : t));
120
+ this.persist();
121
+ }
122
+ /** Connect + discover tools for a server; caches the list (connect errors surface). */
123
+ async discover(serverName) {
124
+ const s = this.servers.get(serverName);
125
+ if (s === undefined)
126
+ return [];
127
+ try {
128
+ // Dynamic import keeps the SDK-backed connector out of the typert-analyzed
129
+ // host face (third-party SDK types aren't resolvable in the gen-typert overlay).
130
+ const { connectMcpServer } = await import('./mcpConnector.js');
131
+ // Resolve secretRef markers to the stored credential values before connecting,
132
+ // so a configured API key is sent as its real value (never the secretRef).
133
+ const resolved = await resolveConfigSecrets(s.config, async (ref) => {
134
+ const candidates = [
135
+ `mcp.${serverName}.env.${ref}`,
136
+ `mcp.${serverName}.header.${ref}`,
137
+ `mcp.${serverName}.query.${ref}`,
138
+ ];
139
+ for (const candidate of candidates) {
140
+ const value = s.credentials.get(candidate);
141
+ if (value !== undefined)
142
+ return value;
143
+ }
144
+ return undefined;
145
+ });
146
+ const connection = await connectMcpServer(resolved);
147
+ // Preserve the user's per-tool enable flags — re-discovering refreshes
148
+ // descriptions/parameters without resetting toggles (new tools default on).
149
+ const prevEnabled = new Map(s.tools.map(t => [t.name, t.enabled]));
150
+ s.tools = connection.tools.map(t => ({ ...t, enabled: prevEnabled.get(t.name) ?? true }));
151
+ s.error = undefined;
152
+ await connection.close();
153
+ this.persist();
154
+ return s.tools;
155
+ }
156
+ catch (error) {
157
+ s.error = String(error);
158
+ this.persist();
159
+ return s.tools;
160
+ }
161
+ }
162
+ /** Re-connect + re-discover every enabled server — boot-time refill of the
163
+ * discovered descriptions/parameters (the persisted block stores only
164
+ * name+enabled). Preserves per-tool enable flags; per-server errors are
165
+ * recorded and do not stop the others. */
166
+ async reconnectAll() {
167
+ for (const server of this.list()) {
168
+ if (!server.enabled)
169
+ continue;
170
+ await this.discover(server.serverName);
171
+ }
172
+ }
173
+ /** A server's most recent connection/dev error. */
174
+ errorOf(serverName) {
175
+ return this.servers.get(serverName)?.error;
176
+ }
177
+ /** Forward one tool call to a server (connect → call → close). Resolves secrets. */
178
+ async callTool(serverName, rawName, args) {
179
+ const s = this.servers.get(serverName);
180
+ if (s === undefined)
181
+ throw new Error(`unknown MCP server: ${serverName}`);
182
+ const resolved = await resolveConfigSecrets(s.config, async (ref) => {
183
+ const candidates = [
184
+ `mcp.${serverName}.env.${ref}`,
185
+ `mcp.${serverName}.header.${ref}`,
186
+ `mcp.${serverName}.query.${ref}`,
187
+ ];
188
+ for (const candidate of candidates) {
189
+ const value = s.credentials.get(candidate);
190
+ if (value !== undefined)
191
+ return value;
192
+ }
193
+ return undefined;
194
+ });
195
+ const { connectMcpServer } = await import('./mcpConnector.js');
196
+ const connection = await connectMcpServer(resolved);
197
+ try {
198
+ return await connection.call(rawName, args);
199
+ }
200
+ finally {
201
+ await connection.close();
202
+ }
203
+ }
204
+ /** Extract credential reference paths from a config (env/header/url-query).
205
+ * Namespaced `mcp.<serverName>.env.<KEY>` so multiple servers don't collide. */
206
+ credentialRefsOf(config) {
207
+ const refs = [];
208
+ for (const [k, v] of config.env ?? [])
209
+ if (isSecretRef(v))
210
+ refs.push(`mcp.${config.serverName}.env.${k}`);
211
+ for (const [k, v] of config.headers ?? [])
212
+ if (isSecretRef(v))
213
+ refs.push(`mcp.${config.serverName}.header.${k}`);
214
+ if (config.url !== undefined) {
215
+ for (const m of config.url.matchAll(/secretRef:([A-Za-z0-9_.-]+)/g)) {
216
+ refs.push(`mcp.${config.serverName}.query.${m[1]}`);
217
+ }
218
+ }
219
+ return refs;
220
+ }
221
+ }
222
+ /** Whether a config value contains an unresolved secret marker (bare or scheme-prefixed). */
223
+ export function isSecretRef(value) {
224
+ return value.includes('secretRef:');
225
+ }
226
+ /** Human label for a namespaced ref (strip the `mcp.<server>.` prefix). */
227
+ function refLabel(ref) {
228
+ const idx = ref.indexOf('.');
229
+ return ref.slice(idx + 1);
230
+ }
231
+ /** The server name a namespaced credential ref belongs to (`mcp.<server>…`). */
232
+ function refServer(ref) {
233
+ const m = /^mcp\.([A-Za-z0-9_-]+)\./.exec(ref);
234
+ return m?.[1] ?? '';
235
+ }
@@ -0,0 +1,45 @@
1
+ import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
2
+ import type { CatalogJsonValue, CatalogMcpTool } from './types.ts';
3
+ /** Deterministic model-facing name for `(serverName, rawName)`. */
4
+ export declare function publicToolName(serverName: string, rawName: string): string;
5
+ /** A tool-invocation forwarder (connects, resolves secrets, calls, closes). */
6
+ export type McpToolCallFn = (serverName: string, rawName: string, args: Record<string, unknown>) => Promise<unknown>;
7
+ /** Extract human-readable text from MCP content blocks (text blocks joined). */
8
+ export declare function extractText(content: readonly unknown[] | undefined): string;
9
+ /** Build a harness `ToolDefinition` for one discovered MCP tool. */
10
+ export declare function mcpToolDefinition(opts: {
11
+ publicName: string;
12
+ serverName: string;
13
+ rawName: string;
14
+ description: string;
15
+ parameters: CatalogJsonValue | undefined;
16
+ callTool: McpToolCallFn;
17
+ }): ToolDefinition;
18
+ /** One desired registration during reconcile. */
19
+ export interface DesiredMcpTool {
20
+ readonly publicName: string;
21
+ readonly serverName: string;
22
+ readonly rawName: string;
23
+ readonly description: string;
24
+ readonly parameters: CatalogJsonValue | undefined;
25
+ }
26
+ /**
27
+ * The tools registry seam. IMPORTANT: `register` must be invoked as a MEMBER
28
+ * call on `tools` (the traceable proxy) — never extracted to a standalone
29
+ * variable — so `this.ctx` (effect/scope ownership) resolves correctly.
30
+ */
31
+ export interface McpToolRegistry {
32
+ register(definition: ToolDefinition): () => void;
33
+ onRegisterError?: (error: unknown) => void;
34
+ }
35
+ /**
36
+ * Reconcile registered MCP tools: dispose every previously-registered tool, then
37
+ * register the desired set. Registration failures are contained per tool.
38
+ */
39
+ export declare function reconcileRegisteredMcpTools(desired: readonly DesiredMcpTool[], registry: McpToolRegistry, current: Map<string, () => void>, callTool: McpToolCallFn): void;
40
+ /** Compute the desired set from enabled servers + their enabled tools. */
41
+ export declare function desiredMcpTools(servers: readonly {
42
+ readonly serverName: string;
43
+ readonly enabled: boolean;
44
+ }[], toolsByServer: Readonly<Record<string, readonly CatalogMcpTool[]>>): DesiredMcpTool[];
45
+ //# sourceMappingURL=mcpTools.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * MCP tool bridge: turns catalog-discovered MCP tools into harness `ToolDefinition`s
3
+ * registered on `ctx.tools` so the model can call them. A bounded subset of the
4
+ * official mcp-client bridge — the common text-result case (no image/attachment
5
+ * projection, no task execution). Model-facing names follow the stable
6
+ * `mcp__<serverName>__<rawName>` contract, normalized to the DeepSeek
7
+ * function-name constraints; the raw name is only ever sent on the wire.
8
+ *
9
+ * Registration must always invoke `ctx.tools.register(definition)` as a MEMBER
10
+ * call on the traceable service proxy (never extract `register` to a standalone
11
+ * variable) — the proxy resolves `this.ctx` (effect/scope ownership) from the
12
+ * calling context, which a bare function reference loses.
13
+ * @module @khorsheed/dsh-capability-catalog/mcpTools
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+ /** DeepSeek function-name contract: at most 64 characters. */
17
+ const MAX_PUBLIC_NAME_LENGTH = 64;
18
+ /** DeepSeek function-name contract: only `[A-Za-z0-9_-]` is allowed. */
19
+ const INVALID_NAME_CHARS = /[^A-Za-z0-9_-]/g;
20
+ /** Hex chars of the SHA-256 identity hash appended on lossy normalization. */
21
+ const HASH_LENGTH = 12;
22
+ /** Deterministic model-facing name for `(serverName, rawName)`. */
23
+ export function publicToolName(serverName, rawName) {
24
+ const joined = `mcp__${serverName}__${rawName}`;
25
+ const normalized = joined.replace(INVALID_NAME_CHARS, '_');
26
+ if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH)
27
+ return normalized;
28
+ const hash = createHash('sha256').update(joined).digest('hex').slice(0, HASH_LENGTH);
29
+ return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`;
30
+ }
31
+ /** Coerce a raw MCP inputSchema into a JSON Schema object the registry accepts. */
32
+ function sanitizeParameters(parameters) {
33
+ if (parameters !== null && typeof parameters === 'object' && !Array.isArray(parameters)) {
34
+ return parameters;
35
+ }
36
+ return { type: 'object', properties: {} };
37
+ }
38
+ /** Map the SDK `callTool` result to the canonical `{ content, structuredContent }`. */
39
+ function normalizeResult(result) {
40
+ const r = (result ?? {});
41
+ if (r.isError === true)
42
+ throw new Error(r.error ?? 'MCP tool reported an error');
43
+ return {
44
+ content: Array.isArray(r.content) ? r.content : [],
45
+ ...(r.structuredContent !== undefined ? { structuredContent: r.structuredContent } : {}),
46
+ };
47
+ }
48
+ /** Extract human-readable text from MCP content blocks (text blocks joined). */
49
+ export function extractText(content) {
50
+ const blocks = Array.isArray(content) ? content : [];
51
+ const texts = blocks
52
+ .filter((b) => typeof b === 'object' && b !== null && b.type === 'text' && typeof b.text === 'string')
53
+ .map((b) => b.text);
54
+ return texts.length > 0 ? texts.join('\n') : JSON.stringify(blocks);
55
+ }
56
+ /** Build a harness `ToolDefinition` for one discovered MCP tool. */
57
+ export function mcpToolDefinition(opts) {
58
+ const { publicName, rawName, serverName, description, parameters, callTool } = opts;
59
+ return {
60
+ name: publicName,
61
+ description,
62
+ parameters: sanitizeParameters(parameters),
63
+ output: {
64
+ schema: {
65
+ type: 'object',
66
+ properties: { content: { type: 'array', items: {} } },
67
+ required: ['content'],
68
+ additionalProperties: true,
69
+ },
70
+ render: (_args, value) => {
71
+ const result = value;
72
+ return [{ type: 'text', text: extractText(result.content) }];
73
+ },
74
+ },
75
+ execute: async (args) => {
76
+ const result = await callTool(serverName, rawName, (args ?? {}));
77
+ return normalizeResult(result);
78
+ },
79
+ };
80
+ }
81
+ /**
82
+ * Reconcile registered MCP tools: dispose every previously-registered tool, then
83
+ * register the desired set. Registration failures are contained per tool.
84
+ */
85
+ export function reconcileRegisteredMcpTools(desired, registry, current, callTool) {
86
+ for (const dispose of [...current.values()])
87
+ dispose();
88
+ current.clear();
89
+ for (const d of desired) {
90
+ try {
91
+ const dispose = registry.register(mcpToolDefinition({
92
+ publicName: d.publicName,
93
+ serverName: d.serverName,
94
+ rawName: d.rawName,
95
+ description: d.description,
96
+ parameters: d.parameters,
97
+ callTool,
98
+ }));
99
+ current.set(d.publicName, dispose);
100
+ }
101
+ catch (error) {
102
+ registry.onRegisterError?.(error);
103
+ }
104
+ }
105
+ }
106
+ /** Compute the desired set from enabled servers + their enabled tools. */
107
+ export function desiredMcpTools(servers, toolsByServer) {
108
+ const desired = [];
109
+ for (const server of servers) {
110
+ if (!server.enabled)
111
+ continue;
112
+ for (const tool of toolsByServer[server.serverName] ?? []) {
113
+ if (!tool.enabled)
114
+ continue;
115
+ desired.push({
116
+ publicName: publicToolName(server.serverName, tool.name),
117
+ serverName: server.serverName,
118
+ rawName: tool.name,
119
+ description: tool.description,
120
+ parameters: tool.parameters,
121
+ });
122
+ }
123
+ }
124
+ return desired;
125
+ }